Skip to content

freeze_server32¤

Create a 32-bit server for inter-process communication.

Example:

from msl.loadlib import freeze_server32
freeze_server32.main(imports="numpy")

Note

There is also a command-line utility to create a new server.

main ¤

main(
    *,
    data=None,
    dest=None,
    imports=None,
    keep_spec=False,
    keep_tk=False,
    skip_32bit_check=False,
    spec=None,
)

Create a frozen server.

This function should be run using a 32-bit Python interpreter with PyInstaller installed.

Parameters:

Name Type Description Default
data str | Iterable[str] | None

The path(s) to additional data files, or directories containing data files, to be added to the frozen server. Each value should be in the form source:dest_dir, where :dest_dir is optional. source is the path to a file (or a directory of files) to add. dest_dir is an optional destination directory, relative to the top-level directory of the frozen server, to add the file(s) to. If dest_dir is not specified, the file(s) will be added to the top-level directory of the server.

None
dest str | None

The destination directory to save the server to. Default is the current working directory.

None
imports str | Iterable[str] | None

The names of additional modules and packages that must be importable on the server.

None
keep_spec bool

By default, the .spec file that is created (during the freezing process) is deleted. Setting this value to True will keep the .spec file, so that it may be modified and then passed as the value to the spec parameter.

False
keep_tk bool

By default, the tkinter package is excluded from the server. Setting this value to True will bundle tkinter with the server.

False
skip_32bit_check bool

In the rare situation that you want to create a frozen 64-bit server, you can set this value to True which skips the requirement that a 32-bit version of Python must be used to create the server. Before you create a 64-bit server, decide if mocking the connection is a better solution for your application.

False
spec str | None

The path to a spec file to use to create the frozen server.

Attention

If a value for spec is specified, then imports or data cannot be specified.

None
Source code in src/msl/loadlib/freeze_server32.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
def main(  # noqa: C901, PLR0912, PLR0913, PLR0915
    *,
    data: str | Iterable[str] | None = None,
    dest: str | None = None,
    imports: str | Iterable[str] | None = None,
    keep_spec: bool = False,
    keep_tk: bool = False,
    skip_32bit_check: bool = False,
    spec: str | None = None,
) -> None:
    """Create a frozen server.

    This function should be run using a 32-bit Python interpreter with
    [PyInstaller](https://www.pyinstaller.org/){:target="_blank"} installed.

    Args:
        data: The path(s) to additional data files, or directories containing
            data files, to be added to the frozen server. Each value should be in
            the form `source:dest_dir`, where `:dest_dir` is optional. `source` is
            the path to a file (or a directory of files) to add. `dest_dir` is an
            optional destination directory, relative to the top-level directory of
            the frozen server, to add the file(s) to. If `dest_dir` is not specified,
            the file(s) will be added to the top-level directory of the server.
        dest: The destination directory to save the server to. Default is
            the current working directory.
        imports: The names of additional modules and packages that must be
            importable on the server.
        keep_spec: By default, the `.spec` file that is created (during the freezing
            process) is deleted. Setting this value to `True` will keep the `.spec` file,
            so that it may be modified and then passed as the value to the `spec` parameter.
        keep_tk: By default, the [tkinter][]{:target="_blank"} package is excluded from the
            server. Setting this value to `True` will bundle `tkinter` with the server.
        skip_32bit_check: In the rare situation that you want to create a
            frozen 64-bit server, you can set this value to `True` which skips
            the requirement that a 32-bit version of Python must be used to create
            the server. Before you create a 64-bit server, decide if
            [mocking][faq-mock] the connection is a better solution for your
            application.
        spec: The path to a [spec]{:target="_blank"} file to use to create the frozen server.
            [spec]: https://pyinstaller.org/en/stable/spec-files.html#using-spec-files

            !!! attention
                If a value for `spec` is specified, then `imports` or `data` cannot be specified.
    """
    if not skip_32bit_check and IS_PYTHON_64BIT:
        msg = ""
        if sys.argv:
            if sys.argv[0].endswith("freeze32"):
                msg = (
                    "\nIf you want to create a 64-bit server, you may "
                    "include the\n--skip-32bit-check flag "
                    "to ignore this requirement."
                )
            else:
                msg = (
                    "\nIf you want to create a 64-bit server, you may "
                    "set the argument\nskip_32bit_check=True "
                    "to ignore this requirement."
                )
        print(f"ERROR! Must freeze the server using a 32-bit version of Python.{msg}", file=sys.stderr)
        return

    try:
        from PyInstaller import (  # type: ignore[import-untyped] # pyright: ignore[reportMissingModuleSource]
            __version__ as pyinstaller_version,
        )
    except ImportError:
        print(
            "ERROR! PyInstaller must be installed to create the server, run:\npip install pyinstaller", file=sys.stderr
        )
        return

    if spec and (imports or data):
        print("ERROR! Cannot specify a spec file and imports/data", file=sys.stderr)
        return

    here = Path(__file__).parent
    dist_path = Path(dest) if dest is not None else Path.cwd()
    server_path = dist_path / server_filename

    tmp_dir = (
        TemporaryDirectory(ignore_cleanup_errors=True) if sys.version_info[:2] >= (3, 10) else TemporaryDirectory()
    )
    work_path = Path(tmp_dir.name)

    # Specifically invoke pyinstaller in the context of the current python interpreter.
    # This fixes the issue where the blind `pyinstaller` invocation points to a 64-bit version.
    cmd: list[str] = [
        sys.executable,
        "-m",
        "PyInstaller",
        "--distpath",
        str(dist_path),
        "--workpath",
        str(work_path),
        "--noconfirm",
        "--clean",
    ]

    if spec is None:
        cmd.extend(["--specpath", str(work_path), "--python-option", "u"])

        if IS_WINDOWS:
            cmd.extend(["--version-file", _create_version_info_file(work_path)])

        cmd.extend(
            [
                "--name",
                server_filename,
                "--onefile",
            ]
        )

        if imports:
            if isinstance(imports, str):
                imports = [imports]

            sys.path.append(str(Path.cwd()))

            missing: list[str] = []
            for module in imports:
                try:
                    _ = import_module(module)
                except ImportError:  # noqa: PERF203
                    missing.append(module)
                else:
                    cmd.extend(["--hidden-import", module])

            if missing:
                print(
                    f"ERROR! The following modules cannot be imported: {' '.join(missing)}\nCannot freeze the server",
                    file=sys.stderr,
                )
                return

        cmd.extend(_get_standard_modules(keep_tk=keep_tk))

        if data:
            major, *_ = pyinstaller_version.split(".")
            sep = os.pathsep if int(major) < 6 else ":"  # noqa: PLR2004

            if isinstance(data, str):
                data = [data]

            for item in data:
                s = item.split(":")
                if len(s) == 1:
                    src = s[0]
                    dst = "."
                elif len(s) == 2:  # noqa: PLR2004
                    src = s[0]
                    dst = s[1] or "."
                else:
                    print(f"ERROR! Invalid data format {item!r}", file=sys.stderr)
                    return

                source = Path(src).resolve()
                if not source.exists():
                    print(f"ERROR! Cannot find {source}", file=sys.stderr)
                    return

                cmd.extend(["--add-data", f"{source}{sep}{dst}"])

        cmd.append(str(here / "start_server32.py"))
    else:
        cmd.append(spec)

    _ = check_call(cmd)  # noqa: S603

    # maybe create the .NET Framework config file
    if imports and ("pythonnet" in imports):
        _ = check_dot_net_config(server_path)

    if keep_spec and not spec:
        print(f"The following files were saved to {dist_path}\n  {server_filename}")

        if Path(f"{server_path}.config").is_file():
            print(f"  {server_path.name}.config")

        spec_file = "server32.spec"
        _ = copy(work_path / f"{server_filename}.spec", dist_path / spec_file)
        print(f"  {spec_file}")

        if IS_WINDOWS:
            file_version_info = "file_version_info.txt"
            _ = copy(work_path / file_version_info, dist_path)
            print(f"  {file_version_info}  (required by the {spec_file} file)")
    else:
        print(f"Server saved to {server_path}")