bin2whl 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bin2whl/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ # ----------------------------------------------------------------------------------------
2
+ # __init__.py
3
+ # -----------
4
+ #
5
+ # bin2whl package initialisation.
6
+ #
7
+ # (c) 2026 WaterJuice — Unlicense; see LICENSE in the project root.
8
+ #
9
+ # Authors
10
+ # -------
11
+ # bena (via Claude)
12
+ #
13
+ # Version History
14
+ # ---------------
15
+ # Mar 2026 - Created
16
+ # ----------------------------------------------------------------------------------------
17
+
18
+ """bin2whl: Build Python wheels from pre-compiled binaries."""
19
+
20
+ from bin2whl.version import VERSION_STR
21
+
22
+ __all__ = ["__version__"]
23
+
24
+ __version__ = VERSION_STR
bin2whl/__main__.py ADDED
@@ -0,0 +1,28 @@
1
+ # ----------------------------------------------------------------------------------------
2
+ # __main__.py
3
+ # -----------
4
+ #
5
+ # Entry point for python -m bin2whl.
6
+ #
7
+ # (c) 2026 WaterJuice — Unlicense; see LICENSE in the project root.
8
+ #
9
+ # Authors
10
+ # -------
11
+ # bena (via Claude)
12
+ #
13
+ # Version History
14
+ # ---------------
15
+ # Mar 2026 - Created
16
+ # ----------------------------------------------------------------------------------------
17
+
18
+ import sys
19
+
20
+ MIN_PYTHON = (3, 12)
21
+ if sys.version_info < MIN_PYTHON:
22
+ print(f"Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+ is required.", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+ from bin2whl.cli import main # noqa: E402
26
+
27
+ if __name__ == "__main__":
28
+ raise SystemExit(main())
bin2whl/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
bin2whl/argbuilder.py ADDED
@@ -0,0 +1,172 @@
1
+ # ----------------------------------------------------------------------------------------
2
+ # argbuilder.py
3
+ # -------------
4
+ #
5
+ # Argument parser wrapper with deferred execution. Provides a thin layer over
6
+ # argparse for consistent CLI argument handling.
7
+ #
8
+ # (c) 2026 WaterJuice — Unlicense; see LICENSE in the project root.
9
+ #
10
+ # Authors
11
+ # -------
12
+ # bena (via Claude)
13
+ #
14
+ # Version History
15
+ # ---------------
16
+ # Mar 2026 - Created
17
+ # ----------------------------------------------------------------------------------------
18
+
19
+ # ----------------------------------------------------------------------------------------
20
+ # Imports
21
+ # ----------------------------------------------------------------------------------------
22
+
23
+ import argparse
24
+ from typing import Any
25
+
26
+ # ----------------------------------------------------------------------------------------
27
+ # Types
28
+ # ----------------------------------------------------------------------------------------
29
+
30
+ Namespace = argparse.Namespace
31
+
32
+ # ----------------------------------------------------------------------------------------
33
+ # Classes
34
+ # ----------------------------------------------------------------------------------------
35
+
36
+
37
+ class ArgsParser:
38
+ """
39
+ Thin wrapper over argparse.ArgumentParser with a simplified interface.
40
+ """
41
+
42
+ # ------------------------------------------------------------------------------------
43
+ def __init__(
44
+ self,
45
+ prog: str,
46
+ description: str,
47
+ version: str,
48
+ ) -> None:
49
+ """
50
+ Initialise the argument parser.
51
+
52
+ Parameters:
53
+ prog: Program name.
54
+ description: Program description shown in help.
55
+ version: Version string shown with --version.
56
+ """
57
+ self._parser = argparse.ArgumentParser(
58
+ prog=prog,
59
+ description=description,
60
+ formatter_class=argparse.RawDescriptionHelpFormatter,
61
+ )
62
+ self._parser.add_argument(
63
+ "--version",
64
+ action="version",
65
+ version=version,
66
+ )
67
+
68
+ # ------------------------------------------------------------------------------------
69
+ def add_argument(self, *args: Any, **kwargs: Any) -> argparse.Action:
70
+ """
71
+ Add an argument to the parser. Passes through to argparse.
72
+
73
+ Parameters:
74
+ args: Positional arguments for argparse.
75
+ kwargs: Keyword arguments for argparse.
76
+
77
+ Returns:
78
+ The created Action.
79
+ """
80
+ return self._parser.add_argument(*args, **kwargs)
81
+
82
+ # ------------------------------------------------------------------------------------
83
+ def add_argument_group(self, title: str) -> "ArgGroup":
84
+ """
85
+ Add a named argument group for organising help output.
86
+
87
+ Parameters:
88
+ title: Group heading shown in help.
89
+
90
+ Returns:
91
+ An ArgGroup wrapper.
92
+ """
93
+ group = self._parser.add_argument_group(title)
94
+ return ArgGroup(group)
95
+
96
+ # ------------------------------------------------------------------------------------
97
+ def add_mutex_group(self) -> "MutexGroup":
98
+ """
99
+ Add a mutually exclusive argument group.
100
+
101
+ Returns:
102
+ A MutexGroup wrapper.
103
+ """
104
+ group = self._parser.add_mutually_exclusive_group()
105
+ return MutexGroup(group)
106
+
107
+ # ------------------------------------------------------------------------------------
108
+ def parse(self, argv: list[str]) -> Namespace:
109
+ """
110
+ Parse arguments and return the namespace.
111
+
112
+ Parameters:
113
+ argv: List of argument strings (without program name).
114
+
115
+ Returns:
116
+ Parsed argument namespace.
117
+ """
118
+ return self._parser.parse_args(argv)
119
+
120
+ # ------------------------------------------------------------------------------------
121
+ def error(self, message: str) -> None:
122
+ """
123
+ Print an error message and exit.
124
+
125
+ Parameters:
126
+ message: The error message to display.
127
+ """
128
+ self._parser.error(message)
129
+
130
+
131
+ class ArgGroup:
132
+ """Wrapper for a named argument group."""
133
+
134
+ # ------------------------------------------------------------------------------------
135
+ def __init__(self, group: argparse._ArgumentGroup) -> None: # pyright: ignore[reportPrivateUsage]
136
+ self._group = group
137
+
138
+ # ------------------------------------------------------------------------------------
139
+ def add_argument(self, *args: Any, **kwargs: Any) -> argparse.Action:
140
+ """
141
+ Add an argument to the group. Passes through to argparse.
142
+
143
+ Parameters:
144
+ args: Positional arguments for argparse.
145
+ kwargs: Keyword arguments for argparse.
146
+
147
+ Returns:
148
+ The created Action.
149
+ """
150
+ return self._group.add_argument(*args, **kwargs)
151
+
152
+
153
+ class MutexGroup:
154
+ """Wrapper for a mutually exclusive argument group."""
155
+
156
+ # ------------------------------------------------------------------------------------
157
+ def __init__(self, group: argparse._MutuallyExclusiveGroup) -> None: # pyright: ignore[reportPrivateUsage]
158
+ self._group = group
159
+
160
+ # ------------------------------------------------------------------------------------
161
+ def add_argument(self, *args: Any, **kwargs: Any) -> argparse.Action:
162
+ """
163
+ Add an argument to the mutex group. Passes through to argparse.
164
+
165
+ Parameters:
166
+ args: Positional arguments for argparse.
167
+ kwargs: Keyword arguments for argparse.
168
+
169
+ Returns:
170
+ The created Action.
171
+ """
172
+ return self._group.add_argument(*args, **kwargs)
bin2whl/cli.py ADDED
@@ -0,0 +1,369 @@
1
+ # ----------------------------------------------------------------------------------------
2
+ # cli.py
3
+ # ------
4
+ #
5
+ # Command-line interface entry point for bin2whl. Supports building Python wheels
6
+ # from pre-compiled binaries, either via a wheel.json config file or CLI arguments.
7
+ #
8
+ # (c) 2026 WaterJuice — Unlicense; see LICENSE in the project root.
9
+ #
10
+ # Authors
11
+ # -------
12
+ # bena (via Claude)
13
+ #
14
+ # Version History
15
+ # ---------------
16
+ # Mar 2026 - Created
17
+ # Apr 2026 - Added multi-binary support
18
+ # ----------------------------------------------------------------------------------------
19
+
20
+ # ----------------------------------------------------------------------------------------
21
+ # Imports
22
+ # ----------------------------------------------------------------------------------------
23
+
24
+ import sys
25
+ import traceback
26
+ from pathlib import Path
27
+ from bin2whl.config import DEFAULT_OUTPUT_DIR
28
+ from bin2whl.config import load_config
29
+ from bin2whl.version import VERSION_STR
30
+ from bin2whl.wheel_builder import BinarySpec
31
+ from bin2whl.wheel_builder import build_wheel
32
+ from .argbuilder import ArgsParser
33
+ from .argbuilder import Namespace
34
+
35
+ # ----------------------------------------------------------------------------------------
36
+ # Constants
37
+ # ----------------------------------------------------------------------------------------
38
+
39
+ LICENSE_TEXT = """\
40
+ This is free and unencumbered software released into the public domain.
41
+
42
+ Anyone is free to copy, modify, publish, use, compile, sell, or
43
+ distribute this software, either in source code form or as a compiled
44
+ binary, for any purpose, commercial or non-commercial, and by any
45
+ means.
46
+
47
+ In jurisdictions that recognize copyright laws, the author or authors
48
+ of this software dedicate any and all copyright interest in the
49
+ software to the public domain. We make this dedication for the benefit
50
+ of the public at large and to the detriment of our heirs and
51
+ successors. We intend this dedication to be an overt act of
52
+ relinquishment in perpetuity of all present and future rights to this
53
+ software under copyright law.
54
+
55
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
56
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
57
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
58
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
59
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
60
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
61
+ OTHER DEALINGS IN THE SOFTWARE.
62
+
63
+ For more information, please refer to <https://unlicense.org>"""
64
+
65
+ EXAMPLE_CONFIG = """\
66
+ Single binary per platform:
67
+
68
+ {
69
+ "name": "your-tool",
70
+ "version": "0.1.0",
71
+ "description": "A CLI tool packaged for PyPI",
72
+ "author": "Your Name",
73
+ "license": "MIT",
74
+ "binaries": {
75
+ "linux_x86_64": "dist/your-tool-linux-x86_64",
76
+ "linux_arm64": "dist/your-tool-linux-arm64",
77
+ "macos_arm64": "dist/your-tool-macos-arm64",
78
+ "windows_amd64": "dist/your-tool-win-x86_64.exe"
79
+ }
80
+ }
81
+
82
+ Multiple binaries per platform:
83
+
84
+ {
85
+ "name": "your-suite",
86
+ "version": "0.1.0",
87
+ "binaries": {
88
+ "linux_x86_64": [
89
+ {"name": "your-server", "path": "dist/server-linux-x86_64"},
90
+ {"name": "your-client", "path": "dist/client-linux-x86_64"}
91
+ ],
92
+ "macos_arm64": [
93
+ {"name": "your-server", "path": "dist/server-darwin-arm64"},
94
+ {"name": "your-client", "path": "dist/client-darwin-arm64"}
95
+ ]
96
+ }
97
+ }"""
98
+
99
+ PLATFORMS_TEXT = """\
100
+ Platform aliases (short names that expand to standard wheel tags):
101
+
102
+ linux_x86_64 → manylinux_2_17_x86_64
103
+ linux_amd64 → manylinux_2_17_x86_64
104
+ linux_aarch64 → manylinux_2_17_aarch64
105
+ linux_arm64 → manylinux_2_17_aarch64
106
+ macos_x86_64 → macosx_10_9_x86_64
107
+ macos_amd64 → macosx_10_9_x86_64
108
+ macos_arm64 → macosx_11_0_arm64
109
+ macos_aarch64 → macosx_11_0_arm64
110
+ windows_amd64 → win_amd64
111
+ windows_x86_64 → win_amd64
112
+ windows_arm64 → win_arm64
113
+
114
+ Any valid wheel platform tag is also accepted directly (e.g. macosx_14_0_arm64)."""
115
+
116
+ # ----------------------------------------------------------------------------------------
117
+ # Functions
118
+ # ----------------------------------------------------------------------------------------
119
+
120
+
121
+ # ----------------------------------------------------------------------------------------
122
+ def parse_args(argv: list[str]) -> Namespace:
123
+ """Parse command-line arguments."""
124
+ p = ArgsParser(
125
+ prog="bin2whl",
126
+ description=(
127
+ f"bin2whl: {VERSION_STR}\n"
128
+ "(c) 2026 WaterJuice. Unlicense.\n\n"
129
+ "Build Python wheels from pre-compiled binaries."
130
+ ),
131
+ version=f"bin2whl: {VERSION_STR}\npython: {sys.version.split()[0]}",
132
+ )
133
+
134
+ p.add_argument(
135
+ "--license",
136
+ action="version",
137
+ version=LICENSE_TEXT,
138
+ help="show license and exit",
139
+ )
140
+ p.add_argument(
141
+ "--example-config",
142
+ action="version",
143
+ version=EXAMPLE_CONFIG,
144
+ help="print an example wheel.json and exit",
145
+ )
146
+ p.add_argument(
147
+ "--platforms",
148
+ action="version",
149
+ version=PLATFORMS_TEXT,
150
+ help="list supported platform tags and exit",
151
+ )
152
+
153
+ common_group = p.add_argument_group("common options")
154
+ common_group.add_argument(
155
+ "--version-str",
156
+ "-v",
157
+ default=None,
158
+ dest="pkg_version",
159
+ help="package version (required for single binary mode, overrides config)",
160
+ )
161
+
162
+ config_group = p.add_argument_group("config file mode")
163
+ config_group.add_argument(
164
+ "--config",
165
+ "-c",
166
+ default=None,
167
+ help="path to config file (e.g. wheel.json)",
168
+ )
169
+
170
+ single_group = p.add_argument_group("single binary mode")
171
+ single_group.add_argument(
172
+ "--name",
173
+ "-n",
174
+ default=None,
175
+ help="package name",
176
+ )
177
+ single_group.add_argument(
178
+ "--binary",
179
+ "-b",
180
+ default=None,
181
+ help="path to binary file",
182
+ )
183
+ single_group.add_argument(
184
+ "--platform",
185
+ "-p",
186
+ default=None,
187
+ help="platform tag (see --platforms)",
188
+ )
189
+ single_group.add_argument(
190
+ "--description",
191
+ "-d",
192
+ default="",
193
+ help="package description",
194
+ )
195
+ single_group.add_argument(
196
+ "--author",
197
+ "-a",
198
+ default="",
199
+ help="author name",
200
+ )
201
+ single_group.add_argument(
202
+ "--author-email",
203
+ "-e",
204
+ default="",
205
+ help="author email",
206
+ )
207
+
208
+ output_group = p.add_argument_group("output")
209
+ output_group.add_argument(
210
+ "--output-dir",
211
+ "-o",
212
+ default=None,
213
+ help="output directory for wheel files (default: wheels)",
214
+ )
215
+
216
+ return p.parse(argv)
217
+
218
+
219
+ # ----------------------------------------------------------------------------------------
220
+ def main(argv: list[str] | None = None) -> int:
221
+ """
222
+ Main entry point for the bin2whl CLI.
223
+
224
+ Parameters:
225
+ argv: Command line arguments (without program name). If None, uses sys.argv[1:].
226
+
227
+ Returns:
228
+ Exit code (0 for success, non-zero for error)
229
+ """
230
+ if argv is None:
231
+ argv = sys.argv[1:]
232
+
233
+ try:
234
+ return _main_inner(argv)
235
+ except KeyboardInterrupt:
236
+ print()
237
+ print("---- Manually Terminated ----")
238
+ print()
239
+ return 1
240
+ except SystemExit:
241
+ raise
242
+ except BaseException as e:
243
+ t = "-----------------------------------------------------------------------------\n"
244
+ t += "UNHANDLED EXCEPTION OCCURRED!!\n"
245
+ t += "\n"
246
+ t += traceback.format_exc()
247
+ t += "\n"
248
+ t += f"EXCEPTION: {type(e)} {e}\n"
249
+ t += "-----------------------------------------------------------------------------\n"
250
+ t += "\n"
251
+ print(t, file=sys.stderr)
252
+ return 1
253
+
254
+
255
+ # ----------------------------------------------------------------------------------------
256
+ def _main_inner(argv: list[str]) -> int:
257
+ """Inner main function that does the actual work."""
258
+ args = parse_args(argv)
259
+
260
+ # Determine mode: single binary or config file
261
+ single_binary_args = [args.name, args.binary, args.platform]
262
+ has_single_args = any(a is not None for a in single_binary_args)
263
+ all_single_args = (
264
+ all(a is not None for a in single_binary_args) and args.pkg_version is not None
265
+ )
266
+
267
+ if has_single_args and not all_single_args:
268
+ print(
269
+ "Error: --name, --version-str, --binary, and --platform are all required "
270
+ "for single binary mode.",
271
+ file=sys.stderr,
272
+ )
273
+ return 1
274
+
275
+ if has_single_args:
276
+ return _build_single(args)
277
+
278
+ # Config file mode requires --config
279
+ if args.config is not None:
280
+ return _build_from_config(args)
281
+
282
+ # No arguments given — show help
283
+ parse_args(["--help"])
284
+ return 0
285
+
286
+
287
+ # ----------------------------------------------------------------------------------------
288
+ def _build_single(args: Namespace) -> int:
289
+ """
290
+ Build a single wheel from CLI arguments.
291
+
292
+ Parameters:
293
+ args: Parsed command-line arguments.
294
+
295
+ Returns:
296
+ Exit code.
297
+ """
298
+ output_dir = Path(args.output_dir) if args.output_dir else Path(DEFAULT_OUTPUT_DIR)
299
+
300
+ try:
301
+ wheel_path = build_wheel(
302
+ binary_path=Path(args.binary),
303
+ name=args.name,
304
+ version=args.pkg_version,
305
+ platform=args.platform,
306
+ output_dir=output_dir,
307
+ description=args.description,
308
+ author=args.author,
309
+ author_email=args.author_email,
310
+ )
311
+ except (FileNotFoundError, IsADirectoryError, ValueError) as e:
312
+ print(f"Error: {e}", file=sys.stderr)
313
+ return 1
314
+
315
+ print(f"Built: {wheel_path}")
316
+ return 0
317
+
318
+
319
+ # ----------------------------------------------------------------------------------------
320
+ def _build_from_config(args: Namespace) -> int:
321
+ """
322
+ Build wheels from a wheel.json configuration file.
323
+
324
+ Parameters:
325
+ args: Parsed command-line arguments.
326
+
327
+ Returns:
328
+ Exit code.
329
+ """
330
+ config_path = Path(args.config)
331
+
332
+ try:
333
+ config = load_config(config_path)
334
+ except (FileNotFoundError, IsADirectoryError, ValueError) as e:
335
+ print(f"Error: {e}", file=sys.stderr)
336
+ return 1
337
+
338
+ version = args.pkg_version if args.pkg_version else config.version
339
+ if not version:
340
+ print(
341
+ "Error: version is required. Provide it in the config file or with --version-str.",
342
+ file=sys.stderr,
343
+ )
344
+ return 1
345
+ output_dir = Path(args.output_dir) if args.output_dir else Path(config.output_dir)
346
+ built_count = 0
347
+
348
+ for binary_mapping in config.binaries:
349
+ specs = [BinarySpec(name=e.name, path=e.path) for e in binary_mapping.entries]
350
+ wheel_path = build_wheel(
351
+ name=config.name,
352
+ version=version,
353
+ platform=binary_mapping.platform,
354
+ output_dir=output_dir,
355
+ description=config.description,
356
+ author=config.author,
357
+ author_email=config.author_email,
358
+ license_name=config.license_name,
359
+ homepage=config.homepage,
360
+ python_requires=config.python_requires,
361
+ classifiers=config.classifiers,
362
+ readme_content=config.readme_content,
363
+ binaries=specs,
364
+ )
365
+ print(f"Built: {wheel_path}")
366
+ built_count += 1
367
+
368
+ print(f"\n{built_count} wheel(s) built in {output_dir}/")
369
+ return 0