zig-zon 0.2.4__tar.gz → 0.2.6__tar.gz

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.
Files changed (33) hide show
  1. {zig-zon-0.2.4 → zig-zon-0.2.6}/PKG-INFO +1 -1
  2. zig-zon-0.2.6/build.zig +119 -0
  3. {zig-zon-0.2.4 → zig-zon-0.2.6}/build.zig.zon +3 -5
  4. zig-zon-0.2.6/setup.py +124 -0
  5. zig-zon-0.2.6/setup.zig.json +1 -0
  6. zig-zon-0.2.6/src/zig_zon.pyi +5 -0
  7. {zig-zon-0.2.4 → zig-zon-0.2.6}/src/zig_zon.zig +39 -13
  8. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/LICENSE +21 -0
  9. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/README.md +275 -0
  10. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/build.zig +296 -0
  11. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8}/build.zig.zon +1 -1
  12. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8}/pkg-info.zon +1 -1
  13. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/src/build/CPython.zig +185 -0
  14. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/src/build/Compression.zig +33 -0
  15. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8}/src/build/SDistStep.zig +36 -20
  16. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8}/src/build/VenvInstallTest.zig +35 -9
  17. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8}/src/build/extract_link_info.py +4 -12
  18. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/src/build/pip_runner.py +153 -0
  19. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/src/build/python-translatec.h +6 -0
  20. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8}/src/build/python.zig +216 -92
  21. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/src/build/python_allocator.zig +201 -0
  22. zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8/src/build/setup_json.zig +34 -0
  23. zig-zon-0.2.4/build.zig +0 -75
  24. zig-zon-0.2.4/setup.py +0 -66
  25. zig-zon-0.2.4/setup.zig.json +0 -1
  26. zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3/README.md +0 -114
  27. zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3/build.zig +0 -159
  28. zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3/src/build/CPython.zig +0 -112
  29. zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3/src/build/setup_json.zig +0 -20
  30. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6}/LICENSE +0 -0
  31. {zig-zon-0.2.4 → zig-zon-0.2.6}/pkg-info.zon +0 -0
  32. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8}/src/build/LowerDashString.zig +0 -0
  33. {zig-zon-0.2.4/zig-pkg/zig_python_sdist-0.0.3 → zig-zon-0.2.6/zig-pkg/zig_python_sdist-0.0.8}/src/build/pkg_info.zig +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zig-zon
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: a native cpython extension to parse zig object notation strings
5
5
  Author-email: Tobias Simetsreiter <dasimmet@gmail.com>
6
6
  Maintainer-email: Tobias Simetsreiter <dasimmet@gmail.com>
@@ -0,0 +1,119 @@
1
+ const std = @import("std");
2
+ const Build = std.Build;
3
+ const zig_python_sdist = @import("zig_python_sdist");
4
+ const zon = @import("build.zig.zon");
5
+
6
+ pub fn build(b: *Build) void {
7
+ const target = b.standardTargetOptions(.{});
8
+ const optimize = b.standardOptimizeOption(.{});
9
+
10
+ // this target and section of our build is used at pip install time
11
+ const ext_install_step = b.step("bdist-wheel", "build python extension module");
12
+ ext_install_step.dependOn(
13
+ &b.addInstallFile(b.path("src/zig_zon.pyi"), "lib/zig_zon.pyi").step,
14
+ );
15
+
16
+ // this generates a python sdist archive
17
+ const sdist = zig_python_sdist.createSdistStep(b, .{
18
+ .name = "zig-zon",
19
+ .version = zon.version,
20
+ .description = b.path("README.md"),
21
+ .build = .{
22
+ .args = &.{
23
+ ext_install_step.name,
24
+ b.fmt("-Doptimize={s}", .{@tagName(optimize)}),
25
+ },
26
+ },
27
+ .extra_pkg_info = @import("pkg-info.zon"),
28
+ .files = &.{
29
+ .{ .src = b.path("LICENSE"), .dest = "LICENSE" },
30
+ .{ .src = b.path("pkg-info.zon"), .dest = "pkg-info.zon" },
31
+ .{ .src = b.path("build.zig.zon"), .dest = "build.zig.zon" },
32
+ .{ .src = b.path("build.zig"), .dest = "build.zig" },
33
+ .{ .src = b.path("src/zig_zon.zig"), .dest = "src/zig_zon.zig" },
34
+ .{ .src = b.path("src/zig_zon.pyi"), .dest = "src/zig_zon.pyi" },
35
+ },
36
+ .compression = .best,
37
+ });
38
+
39
+ const sdistfile = b.addInstallFileWithDir(
40
+ sdist.getEmittedArchive(),
41
+ .{ .custom = "src" },
42
+ sdist.basename,
43
+ );
44
+ b.getInstallStep().dependOn(&sdistfile.step);
45
+
46
+ // we can use a python virtual environment inside
47
+ // the zig cache to test pip installation
48
+ const test_pip_install = zig_python_sdist.createVenvInstallTest(
49
+ b,
50
+ .{
51
+ .sdist = sdist.getEmittedArchive(),
52
+ .unittest_py = &.{b.path("src/test_zig_zon.py")},
53
+ .twine_check = true,
54
+ .no_clean = b.option(bool, "pip-no-clean", "dont cleanup pip temp dirs") orelse false,
55
+ },
56
+ );
57
+ b.step("test-pip-install", "").dependOn(test_pip_install.step);
58
+
59
+ const cpython = zig_python_sdist.createCPythonModule(b, .{
60
+ .target = target,
61
+ .version = zon.version,
62
+ .optimize = .ReleaseSmall,
63
+ });
64
+ const lib = b.addLibrary(.{
65
+ .name = "zig_zon",
66
+ .root_module = b.addModule("zig_zon", .{
67
+ .root_source_file = b.path("src/zig_zon.zig"),
68
+ .target = target,
69
+ .optimize = optimize,
70
+ .imports = &.{
71
+ .{ .name = "cpython", .module = cpython },
72
+ },
73
+ }),
74
+ .linkage = .dynamic,
75
+ });
76
+ const ext_install = zig_python_sdist.installExtLib(b, lib);
77
+ ext_install_step.dependOn(&ext_install.step);
78
+
79
+ ext_install_step.dependOn(&ext_install.step);
80
+
81
+ const ext_path = zig_python_sdist.CPython.renameExtLib(b, lib);
82
+
83
+ {
84
+ const ext_tests = ext_path.wf.addCopyFile(b.path("src/test_zig_zon.py"), "test_zig_zon.py");
85
+ _ = ext_path.wf.addCopyFile(b.path("build.zig.zon"), "build.zig.zon");
86
+ const test_run = Build.Step.Run.create(b, "unittest");
87
+ test_run.setCwd(ext_path.wf.getDirectory());
88
+ test_run.setEnvironmentVariable("PYTHONPATH", ".");
89
+ test_run.addFileArg(zig_python_sdist.CPython.find(b));
90
+ if (b.option(bool, "tracetest", "run unittest with tracing") orelse false) {
91
+ test_run.addArgs(&.{ "-m", "trace", "--trace" });
92
+ } else {
93
+ test_run.addArgs(&.{ "-m", "unittest" });
94
+ }
95
+ test_run.addDirectoryArg(ext_tests);
96
+ b.step("test-bdist", "run unittests against the extension").dependOn(&test_run.step);
97
+ }
98
+
99
+ const usf = b.addUpdateSourceFiles();
100
+ b.step("update-src", "updates the pyi file. needs 'mypy' installed").dependOn(&usf.step);
101
+
102
+ {
103
+ const stubgen_run = Build.Step.Run.create(b, "stubgen");
104
+ stubgen_run.setCwd(ext_path.wf.getDirectory());
105
+ stubgen_run.addArgs(&.{
106
+ "stubgen",
107
+ "-m",
108
+ lib.name,
109
+ "-o",
110
+ });
111
+ const stub_dir = stubgen_run.addOutputDirectoryArg("stubgen");
112
+ stubgen_run.addArg("--search-path");
113
+ stubgen_run.addDirectoryArg(ext_path.wf.getDirectory());
114
+ usf.addCopyFileToSource(
115
+ stub_dir.path(b, b.fmt("{s}.pyi", .{lib.name})),
116
+ b.fmt("src/{s}.pyi", .{lib.name}),
117
+ );
118
+ }
119
+ }
@@ -1,12 +1,12 @@
1
1
  .{
2
2
  .name = .python_zig_zon,
3
3
  .fingerprint = 0x3d332656a3644fc9,
4
- .version = "0.2.4",
4
+ .version = "0.2.6",
5
5
  .minimum_zig_version = "0.16.0",
6
6
  .dependencies = .{
7
7
  .zig_python_sdist = .{
8
- .url = "git+https://codeberg.org/dasimmet/zig-python-sdist.git#b09744edafe683ae6000a96498cc68e86173dbe7",
9
- .hash = "zig_python_sdist-0.0.3-wjgdOlzvAADTtFV1n4xTLUwlgCsiLDcmhebfZ7kWM05z",
8
+ .url = "git+https://codeberg.org/dasimmet/zig-python-sdist.git?ref=0.0.8#3f06dd8177fbba62a68b7f0894b4a3d26cfb9fcf",
9
+ .hash = "zig_python_sdist-0.0.8-wjgdOimGAQBiUtyW-KGTUzlNhuwanlP9ealvenf3vcpw",
10
10
  },
11
11
  },
12
12
  .paths = .{
@@ -16,7 +16,5 @@
16
16
  "pkg-info.zon",
17
17
  "LICENSE",
18
18
  "README.md",
19
- "setup.py",
20
- "pyproject.toml",
21
19
  },
22
20
  }
zig-zon-0.2.6/setup.py ADDED
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python
2
+
3
+ from setuptools import setup
4
+ from setuptools.dist import Distribution
5
+ from setuptools.command.bdist_wheel import bdist_wheel
6
+ import subprocess
7
+ import sys
8
+ import os
9
+ import json
10
+ import glob
11
+ import shutil
12
+
13
+ with open("setup.zig.json") as fd:
14
+ setup_json = json.load(fd)
15
+
16
+
17
+ def mkversion(v, need_release=False):
18
+ '''assembles a dict of semantic version to string'''
19
+ if need_release:
20
+ assert v.get('pre') is None, "cannot use version with pre: " + str(v)
21
+ assert v.get(
22
+ 'build') is None, "cannot use version with build: " + str(v)
23
+ return str(v['major']) + '.' + str(
24
+ v['minor']) + '.' + str(v['patch'])
25
+
26
+
27
+ class ZigDistribution(Distribution):
28
+ def has_ext_modules(self):
29
+ return setup_json['has_ext_modules']
30
+
31
+
32
+ class ZigBuildWheel(bdist_wheel):
33
+ def initialize_options(self):
34
+ super().initialize_options()
35
+ if 'py_limited_api' in setup_json and setup_json['py_limited_api']:
36
+ self.py_limited_api = setup_json['py_limited_api']
37
+
38
+ def run(self, *args, **kwargs):
39
+ print("py_limited_api:", self.py_limited_api, file=sys.stderr)
40
+ print("plat_name:", self.plat_name, file=sys.stderr)
41
+
42
+ import ziglang
43
+ zig_exe = os.path.join(
44
+ os.path.dirname(ziglang.__file__), "zig")
45
+
46
+ cwd = os.getcwd()
47
+ print("zig build dir:", cwd, file=sys.stderr)
48
+ print("using zig exe:", zig_exe, file=sys.stderr)
49
+
50
+ extra_build_args = setup_json['build']['args']
51
+ build_cmd = [zig_exe, 'build'] + extra_build_args
52
+
53
+ print("zig build:", build_cmd, file=sys.stderr)
54
+ subprocess.check_call(build_cmd)
55
+
56
+ os.makedirs(self.bdist_dir, exist_ok=True)
57
+ copy_patterns = setup_json['build']['copy_patterns']
58
+ pkg_name = setup_json['name'].replace('-', '_')
59
+
60
+ for item in copy_patterns:
61
+ pattern = item['pattern']
62
+ dest = item.get('dest')
63
+ package_dir = item.get('package_dir', False)
64
+
65
+ if package_dir:
66
+ if dest:
67
+ dest = os.path.join(pkg_name, dest)
68
+ else:
69
+ dest = pkg_name
70
+ elif dest:
71
+ dest = dest.replace('{name}', pkg_name).replace(
72
+ '{package}', pkg_name)
73
+
74
+ artifacts = glob.glob(pattern)
75
+ if len(artifacts) == 0:
76
+ raise Exception(
77
+ 'no zig artifacts found in {}/{}'.format(cwd, pattern))
78
+
79
+ for it in artifacts:
80
+ if dest:
81
+ is_dir = (
82
+ dest.endswith('/') or
83
+ dest.endswith('\\') or
84
+ any(c in pattern for c in '*?[]') or
85
+ (os.path.splitext(dest)[
86
+ 1] == '' and os.path.splitext(it)[1] != '')
87
+ )
88
+ if is_dir:
89
+ dst = os.path.join(
90
+ self.bdist_dir, dest, os.path.basename(it))
91
+ else:
92
+ dst = os.path.join(self.bdist_dir, dest)
93
+ else:
94
+ dst = os.path.join(self.bdist_dir, os.path.basename(it))
95
+
96
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
97
+ print("cp", it, dst, file=sys.stderr)
98
+ if os.path.isdir(it):
99
+ shutil.copytree(it, dst, dirs_exist_ok=True)
100
+ else:
101
+ shutil.copyfile(it, dst)
102
+
103
+ return super().run(*args, **kwargs)
104
+
105
+
106
+ options = {}
107
+ if 'py_limited_api' in setup_json and setup_json['py_limited_api']:
108
+ options['bdist_wheel'] = {'py_limited_api': setup_json['py_limited_api']}
109
+
110
+ setup(
111
+ name=setup_json['name'],
112
+ version=mkversion(setup_json['version']),
113
+ cmdclass={'bdist_wheel': ZigBuildWheel},
114
+ options=options,
115
+ install_requires=setup_json['install_requires'] if 'install_requires' in setup_json else None,
116
+ entry_points=setup_json['entry_points'] if 'entry_points' in setup_json else None,
117
+ packages=setup_json['build']['packages'],
118
+ distclass=ZigDistribution,
119
+ setup_requires=[
120
+ "setuptools",
121
+ "wheel",
122
+ "ziglang==" + mkversion(setup_json['zig_version'], need_release=True),
123
+ ] + setup_json['build']['requires'],
124
+ )
@@ -0,0 +1 @@
1
+ {"name":"zig_zon","version":{"major":0,"minor":2,"patch":6},"zig_version":{"major":0,"minor":16,"patch":0},"build":{"args":["--fork=zig-pkg/zig_python_sdist-0.0.8","bdist-wheel","-Doptimize=ReleaseSmall"],"requires":[],"packages":[],"copy_patterns":[{"pattern":"zig-out/lib/*","package_dir":false}]},"has_ext_modules":true}
@@ -0,0 +1,5 @@
1
+ __version__: dict
2
+ __version_str__: str
3
+ __zig_builtin__: dict
4
+
5
+ def parse(*args, **kwargs): ...
@@ -8,28 +8,54 @@ const cpython = @import("cpython");
8
8
  const c = cpython.c;
9
9
  const PyObject = c.PyObject;
10
10
 
11
- pub var methods = [_]c.PyMethodDef{
12
- c.PyMethodDef{
13
- .ml_name = "parse",
14
- .ml_meth = @ptrCast(@alignCast(&parse)),
15
- .ml_flags = @as(c_int, 1),
16
- .ml_doc = "parse a zon literal to a python object",
17
- },
18
- c.PyMethodDef{},
19
- };
20
-
21
- pub var zigmodule = c.PyModuleDef{
11
+ pub var zigmodule: c.PyModuleDef = .{
22
12
  .m_name = "zig_zon",
23
- .m_methods = &methods,
13
+ .m_methods = @constCast(&[_]c.PyMethodDef{
14
+ .{
15
+ .ml_name = "parse",
16
+ .ml_meth = @ptrCast(@alignCast(&parse)),
17
+ .ml_flags = 1,
18
+ .ml_doc = "parse a zon literal to a python object",
19
+ },
20
+ .{},
21
+ }),
24
22
  };
25
23
 
26
24
  pub export fn PyInit_zig_zon() [*c]c.PyObject {
27
25
  const mod = c.PyModule_Create(&zigmodule);
26
+
27
+ {
28
+ // const annotations = c.PyDict_New();
29
+ // defer c.Py_DecRef(annotations);
30
+ // const builtins = c.PyImport_ImportModule("builtins");
31
+ // defer c.Py_DecRef(builtins);
32
+
33
+ // const str_type = c.PyObject_GetAttrString(builtins, "str") orelse PyErrPanic();
34
+ // defer c.Py_DecRef(str_type);
35
+
36
+ // assert(0 <= c.PyDict_SetItemString(annotations, "value", str_type));
37
+
38
+ // const parse_ref = c.PyObject_GetAttrString(mod, "parse") orelse PyErrPanic();
39
+ // defer c.Py_DecRef(parse_ref);
40
+
41
+ //TODO: find another way to set annotations, since a builtin function is immutable.
42
+
43
+ // if (0 > c.PyObject_SetAttrString(parse_ref, "__annotations__", annotations)) {
44
+ // c.PyErr_Print();
45
+ // @panic("WTF");
46
+ // }
47
+ }
48
+
28
49
  cpython.addVersion(mod);
29
50
  cpython.addZigBuiltin(mod);
30
51
  return mod;
31
52
  }
32
53
 
54
+ pub fn PyErrPanic() noreturn {
55
+ c.PyErr_Print();
56
+ @panic("Unexpected PyErr!");
57
+ }
58
+
33
59
  pub fn parse(self: [*]PyObject, args: [*]PyObject) callconv(.c) [*c]PyObject {
34
60
  _ = self;
35
61
  const zon_slice: [:0]u8 = zon_slice: {
@@ -38,7 +64,7 @@ pub fn parse(self: [*]PyObject, args: [*]PyObject) callconv(.c) [*c]PyObject {
38
64
  break :zon_slice std.mem.sliceTo(zon_txt, 0);
39
65
  };
40
66
 
41
- var arena_instance = std.heap.ArenaAllocator.init(std.heap.c_allocator);
67
+ var arena_instance = std.heap.ArenaAllocator.init(cpython.py_allocator);
42
68
  defer arena_instance.deinit();
43
69
  const arena = arena_instance.allocator();
44
70
 
@@ -0,0 +1,21 @@
1
+ The MIT License (Expat)
2
+
3
+ Copyright (c) Zig contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,275 @@
1
+ # Zig Python Sdist
2
+
3
+ A toolset to build Python source distributions (`sdist`) and wheels directly with `zig build`. Inspired by Loris Cro's talk [Deinventing the Wheel](https://www.youtube.com/watch?v=HPmefnqirHk).
4
+
5
+ `zig-python-sdist` packages your Zig project into a standard `.tar.gz` source distribution for PyPI. When a user runs `pip install your-package`, `pip` automatically installs the `ziglang` compiler wheel from PyPI and invokes `zig build` to compile native extensions on the target machine—no manual Python packaging toolchain (C/C++ compilers, CMake, setup.py boilerplate) required.
6
+
7
+ A complete runnable example is available in the [`example/`](example/) directory.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ Add `zig-python-sdist` to your project's `build.zig.zon` (requires Zig >= 0.16.0):
14
+
15
+ ```bash
16
+ zig fetch --save git+https://codeberg.org/dasimmet/zig-python-sdist
17
+ ```
18
+
19
+ Then import it in `build.zig`:
20
+
21
+ ```zig
22
+ const zig_python_sdist = @import("zig_python_sdist");
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Usage Scenarios
28
+
29
+ `zig-python-sdist` supports two main workflows:
30
+ 1. **CPython Native Extension**: A compiled module (`.so` / `.pyd`) using CPython headers and the Python C API (supporting the Python Limited API / `abi3`).
31
+ 2. **Ctypes Wrapper**: A standalone shared library accompanied by Python files loaded via `ctypes`.
32
+
33
+ ---
34
+
35
+ ### 1. CPython Extension (Limited API / `abi3`)
36
+
37
+ #### `build.zig`
38
+
39
+ ```zig
40
+ const std = @import("std");
41
+ const zig_python_sdist = @import("zig_python_sdist");
42
+ const zon = @import("build.zig.zon");
43
+
44
+ pub fn build(b: *std.Build) void {
45
+ const target = b.standardTargetOptions(.{});
46
+ const optimize = b.standardOptimizeOption(.{});
47
+
48
+ // 1. Step executed by pip at wheel build time
49
+ const build_ext_step = b.step("bdist-wheel-cpython", "build the extension module");
50
+
51
+ // 2. Generate CPython module and translate Python headers
52
+ const cpython = zig_python_sdist.createCPythonModule(b, .{
53
+ .target = target,
54
+ .optimize = optimize,
55
+ .version = zon.version,
56
+ .py_limited_api = "cp39", // sets Py_LIMITED_API for Python 3.9+
57
+ });
58
+
59
+ // 3. Build the dynamic extension module
60
+ const example_cpython = b.addLibrary(.{
61
+ .name = "example_cpython",
62
+ .linkage = .dynamic,
63
+ .root_module = b.createModule(.{
64
+ .root_source_file = b.path("example_cpython.zig"),
65
+ .target = target,
66
+ .optimize = optimize,
67
+ .imports = &.{
68
+ .{ .name = "cpython", .module = cpython },
69
+ },
70
+ }),
71
+ });
72
+
73
+ // Install extension to zig-out/lib/
74
+ const example_cpython_install = zig_python_sdist.installExtLib(b, example_cpython);
75
+ build_ext_step.dependOn(&example_cpython_install.step);
76
+
77
+ // 4. Define the sdist archive
78
+ const sdist = zig_python_sdist.createSdistStep(b, .{
79
+ .name = "example-cpython",
80
+ .version = zon.version,
81
+ .description = b.path("README.md"),
82
+ .py_limited_api = "cp39", // tags wheel with cp39-abi3
83
+ .build = .{
84
+ .args = &.{
85
+ build_ext_step.name,
86
+ b.fmt("-Doptimize={s}", .{@tagName(optimize)}),
87
+ },
88
+ .copy_patterns = &.{
89
+ .{ .pattern = "zig-out/lib/*" },
90
+ .{ .pattern = "example_cpython.pyi" },
91
+ },
92
+ },
93
+ .files = &.{
94
+ .{ .src = b.path("build.zig.zon"), .dest = "build.zig.zon" },
95
+ .{ .src = b.path("build.zig"), .dest = "build.zig" },
96
+ .{ .src = b.path("example_cpython.zig"), .dest = "example_cpython.zig" },
97
+ .{ .src = b.path("example_cpython.pyi"), .dest = "example_cpython.pyi" },
98
+ .{ .src = b.path("README.md"), .dest = "README.md" },
99
+ .{ .src = b.path("LICENSE"), .dest = "LICENSE" },
100
+ },
101
+ });
102
+
103
+ // Building the default install step produces the .tar.gz sdist archive
104
+ const sdist_install = sdist.install();
105
+ b.getInstallStep().dependOn(&sdist_install.step);
106
+ }
107
+ ```
108
+
109
+ #### `example_cpython.zig`
110
+
111
+ ```zig
112
+ const std = @import("std");
113
+ const python = @import("cpython");
114
+ const c = python.c;
115
+
116
+ pub export fn PyInit_example_cpython() [*c]c.PyObject {
117
+ const mod = c.PyModule_Create(&zigmodule);
118
+ python.addVersion(mod);
119
+ python.addZigBuiltin(mod);
120
+ return mod;
121
+ }
122
+
123
+ pub var zigmodule: c.PyModuleDef = .{
124
+ .m_name = "example_cpython",
125
+ .m_methods = @constCast(&[_]c.PyMethodDef{
126
+ .{
127
+ .ml_name = "__main__",
128
+ .ml_meth = @ptrCast(@alignCast(&python.wrapMain(main, .{}))),
129
+ .ml_flags = @as(c_int, 1),
130
+ .ml_doc = "main script entrypoint",
131
+ },
132
+ .{},
133
+ }),
134
+ };
135
+
136
+ pub fn main(init: std.process.Init) !void {
137
+ const stdout = std.Io.File.stdout();
138
+ var buf: [4096]u8 = undefined;
139
+ var w = stdout.writer(init.io, &buf);
140
+ try w.interface.print("Hello from Zig!\n", .{});
141
+ try w.interface.flush();
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ### 2. Ctypes Dynamic Library (Pure Wheel)
148
+
149
+ When distributing a shared library called through Python's `ctypes`, you can set `has_ext_modules = false` so setuptools produces a pure wheel (`py3-none-any.whl`), and use `package_dir = true` to place the library and `__init__.py` inside the package directory.
150
+
151
+ #### `build.zig`
152
+
153
+ ```zig
154
+ const example_ctypes = b.addLibrary(.{
155
+ .name = "example_ctypes",
156
+ .root_module = b.createModule(.{
157
+ .root_source_file = b.path("example_ctypes.zig"),
158
+ .target = target,
159
+ .optimize = optimize,
160
+ .link_libc = false,
161
+ }),
162
+ .linkage = .dynamic,
163
+ });
164
+ example_ctypes.rdynamic = true;
165
+
166
+ const build_ctypes_step = b.step("bdist-wheel-ctypes", "build shared library");
167
+ build_ctypes_step.dependOn(&b.addInstallArtifact(example_ctypes, .{}).step);
168
+
169
+ const sdist_ctypes = zig_python_sdist.createSdistStep(b, .{
170
+ .name = "example-ctypes",
171
+ .version = zon.version,
172
+ .description = b.path("README.md"),
173
+ .has_ext_modules = false, // builds a py3-none-any wheel
174
+ .build = .{
175
+ .args = &.{
176
+ b.fmt("-Doptimize={s}", .{@tagName(optimize)}),
177
+ build_ctypes_step.name,
178
+ },
179
+ .copy_patterns = &.{
180
+ // places compiled shared library into example_ctypes/
181
+ .{ .pattern = "zig-out/*/*", .package_dir = true },
182
+ // renames example_ctypes.py to __init__.py inside example_ctypes/
183
+ .{ .pattern = "example_ctypes.py", .dest = "__init__.py", .package_dir = true },
184
+ },
185
+ },
186
+ .files = &.{
187
+ .{ .src = b.path("build.zig.zon"), .dest = "build.zig.zon" },
188
+ .{ .src = b.path("build.zig"), .dest = "build.zig" },
189
+ .{ .src = b.path("example_ctypes.zig"), .dest = "example_ctypes.zig" },
190
+ .{ .src = b.path("example_ctypes.py"), .dest = "example_ctypes.py" },
191
+ },
192
+ });
193
+ ```
194
+
195
+ ---
196
+
197
+ ## API Reference
198
+
199
+ ### `createCPythonModule(b, options)`
200
+
201
+ Generates a Zig module linked against CPython headers:
202
+ - `target`: Target architecture and OS.
203
+ - `optimize`: Optimization mode.
204
+ - `version`: Package version string.
205
+ - `py_limited_api`: Python tag (e.g., `"cp39"`, `"cp310"`) or hex version (e.g. `"0x03090000"`). When set, `#define Py_LIMITED_API` is enabled during header translation.
206
+ - `header_file`: Optional custom C header file (defaults to `python-translatec.h`).
207
+ - `python_exe`: Optional path to Python executable used for finding header search paths.
208
+ - `macros`: Additional C macro definitions.
209
+
210
+ ### `createSdistStep(b, options)`
211
+
212
+ Creates an `SDistStep` that packages source files into an sdist archive:
213
+ - `name`: Distribution name on PyPI.
214
+ - `version`: Package version string.
215
+ - `description`: Path to README file (e.g. `b.path("README.md")`). Added to `PKG-INFO`.
216
+ - `py_limited_api`: Python tag (e.g. `"cp39"`) passed to setuptools to build an `abi3` wheel.
217
+ - `has_ext_modules`: Set to `false` for pure-Python or ctypes packages (defaults to `true`).
218
+ - `build`: Configuration for building wheels:
219
+ - `args`: Command-line arguments forwarded to `zig build` when `pip` builds the wheel.
220
+ - `copy_patterns`: Files and directories to copy into the wheel:
221
+ - `pattern`: Glob pattern (e.g. `"zig-out/lib/*"`).
222
+ - `dest`: Optional destination path or filename.
223
+ - `package_dir`: If `true`, prefixes destination with the package name.
224
+ - `packages`: Python packages to include.
225
+ - `requires`: Additional Python build dependencies.
226
+ - `files`: Slice of `.{ .src = LazyPath, .dest = []const u8 }` to include in the archive.
227
+ - `directories`: Slice of directory paths to include in the archive.
228
+ - `entry_points`: Command-line scripts, e.g. `.{ .console_scripts = &.{ "my-cli = my_pkg:__main__" } }`.
229
+ - `install_requires`: Slice of runtime dependencies (e.g. `&.{ "requests>=2.28" }`).
230
+ - `extra_pkg_info`: Extra metadata headers added to `PKG-INFO`.
231
+ - `compression`: Compression level (`.default`, `.best`, `.fast`).
232
+
233
+ > [!TIP]
234
+ > When your package uses dependencies from `build.zig.zon`, package the dependency directory into the sdist and pass `--fork=zig-pkg/<dep>` in `build.args` so `zig build` does not re-download it during pip install. `zig-python-sdist` automatically adds itself this way.
235
+
236
+ ---
237
+
238
+ ## The `cpython` Helper Library
239
+
240
+ The imported `cpython` module provides:
241
+ - **`c`**: Direct access to translated Python C API headers (`Python.h`).
242
+ - **`py_allocator`**: A `std.mem.Allocator` implementation backed by Python's `PyMem_RawMalloc` / `PyMem_RawFree`.
243
+ - **`wrapMain`**: Wraps a Zig main function (`fn (std.process.Init) !void` or `fn (std.process.Init.Minimal) !void`) into a CPython entrypoint function (`callconv(.c) [*c]PyObject`). CLI arguments and environment variables are automatically populated from Python's `sys.argv` and `os.environ`.
244
+ - **`addVersion`**: Adds `__version__` and `__version_str__` attributes to a Python module.
245
+ - **`addZigBuiltin`**: Adds a `__zig_builtin__` dictionary with Zig compiler target and build information.
246
+
247
+ ---
248
+
249
+ ## Testing Installations (`createVenvInstallTest`)
250
+
251
+ You can verify the source distribution end-to-end by building and installing it into a temporary virtual environment:
252
+
253
+ ```zig
254
+ const test_pip = zig_python_sdist.createVenvInstallTest(b, .{
255
+ .sdist = sdist.getEmittedArchive(),
256
+ .unittest_py = &.{b.path("test/test_cpython_ext.py")},
257
+ .twine_check = true, // runs `twine check --strict` on the archive
258
+ .no_clean = b.option(bool, "pip-no-clean", "keep pip temp dirs") orelse false,
259
+ });
260
+ b.step("test-pip-install", "test installing sdist with pip").dependOn(test_pip.step);
261
+ ```
262
+
263
+ Run tests with:
264
+
265
+ ```bash
266
+ zig build test-pip-install
267
+ ```
268
+
269
+ ---
270
+
271
+ ## References
272
+
273
+ - [Deinventing the Wheel - Loris Cro](https://www.youtube.com/watch?v=HPmefnqirHk)
274
+ - [Python C API - Limited API](https://docs.python.org/3/c-api/stable.html)
275
+ - [PEP 384 – Defining a Stable ABI](https://peps.python.org/pep-0384/)