11#!/usr/bin/env python3
22# scripts/create_package.py
33"""
4- Create a deterministic .pkg from a package source dir, using local `arch` if available.
5- If `arch` is missing, this script will attempt to compile src/arch.c into a local arch binary.
4+ Create a deterministic .pkg from a package source dir, always using local build/arch.
65
76Usage:
8- scripts/create_package.py <src_dir> [--out-root pkgs] [--keep-temp] [--arch-cmd PATH]
9-
10- Examples:
11- scripts/create_package.py pkgs/src/mytool/3.0.0 --out-root pkgs
7+ scripts/create_package.py <src_dir> [--out-root pkgs] [--keep-temp]
128
139Behavior:
1410- Expects a manifest.acl file at the top of <src_dir>.
1511- Produces <out_root>/<name>/<ver>/<name>-<ver>.pkg
16- - If ' arch' executable is found in PATH or provided via -- arch-cmd, calls: arch <src_dir> <out_pkg>
17- - Otherwise attempts to compile src/ arch.c to ./build/arch and use it; if compilation fails, falls back to deterministic tar.gz .
12+ - ALWAYS uses build/ arch. If build/arch is missing, attempts to compile src/ arch.c -> build/arch.
13+ - If compilation or arch invocation fails, the script exits with non-zero (no tar fallback) .
1814- Computes SHA256 and updates (or inserts) archive_sha256 in manifest.acl.
1915"""
2016from __future__ import annotations
@@ -47,13 +43,12 @@ def find_manifest(src: Path) -> Path:
4743def update_manifest_archive_sha (manifest_path : Path , sha_hex : str ) -> None :
4844 text = manifest_path .read_text (encoding = "utf-8" )
4945 new_line = f' string archive_sha256 = "{ sha_hex } ";'
50- # replace existing archive_sha or archive_sha256 line if present
5146 pat = re .compile (r'^\s*(string\s+)?archive_sha(?:256)?\s*=\s*".*"\s*;\s*$' , re .MULTILINE )
5247 if pat .search (text ):
5348 text = pat .sub (new_line , text , count = 1 )
5449 else :
55- # attempt to insert before closing brace of first top-level block
56- m = re .search (r'^[a-zA-Z_][a-zA-Z0 -9_]*\s*\{' , text , re .MULTILINE )
50+ # insert before the first top-level closing brace if possible
51+ m = re .search (r'^[A-Za-z_][A-Za-z0 -9_]*\s*\{' , text , re .MULTILINE )
5752 if m :
5853 start = m .end ()
5954 depth = 1
@@ -76,114 +71,65 @@ def update_manifest_archive_sha(manifest_path: Path, sha_hex: str) -> None:
7671def ensure_parent (path : Path ) -> None :
7772 path .parent .mkdir (parents = True , exist_ok = True )
7873
79- # -------- arch build / invocation --------
80-
81- def try_find_arch (provided : Optional [str ]) -> Optional [Path ]:
82- # If user provided explicit arch-cmd, prefer that (verify executable)
83- if provided :
84- p = Path (provided )
85- if p .is_file () and os .access (p , os .X_OK ):
86- return p
87- # maybe it's in PATH; shutil.which handles that
88- which = shutil .which (provided )
89- if which :
90- return Path (which )
91- return None
92- # look in PATH
93- which = shutil .which ("arch" )
94- if which :
95- return Path (which )
96- # try local build/arch
97- local = Path ("build" ) / "arch"
98- if local .is_file () and os .access (local , os .X_OK ):
99- return local
100- # try .local/bin/arch
101- local2 = Path (".local" ) / "bin" / "arch"
102- if local2 .is_file () and os .access (local2 , os .X_OK ):
103- return local2
104- return None
74+ # -------- arch build / invocation (always use build/arch) --------
10575
106- def attempt_build_arch ( src_dir : Path ) -> Optional [ Path ] :
76+ def arch_executable ( ) -> Path :
10777 """
108- Try to compile src/arch.c into build/arch (or .local/bin/arch) .
109- Returns Path to compiled arch on success, or None on failure .
78+ Return the Path to build/arch. If missing, attempt to compile src/arch.c into it .
79+ If compilation fails, raise RuntimeError .
11080 """
81+ out_path = Path ("build" ) / "arch"
82+ if out_path .is_file () and os .access (out_path , os .X_OK ):
83+ return out_path
84+
85+ # attempt to build from src/arch.c
11186 src_c = Path ("src" ) / "arch.c"
11287 if not src_c .is_file ():
113- return None
114- out_dir = Path ("build" )
88+ raise RuntimeError ("build/arch not found and src/arch.c missing; cannot proceed" )
89+
90+ out_dir = out_path .parent
11591 out_dir .mkdir (parents = True , exist_ok = True )
116- out_path = out_dir / "arch"
92+
93+ # prefer cc/gcc/clang
11794 cc = shutil .which ("cc" ) or shutil .which ("gcc" ) or shutil .which ("clang" )
11895 if not cc :
119- return None
96+ raise RuntimeError ("C compiler not found; cannot build build/arch" )
97+
12098 cmd = [cc , "-O2" , "-std=c11" , "-o" , str (out_path ), str (src_c )]
121- print ("Compiling arch:" , " " .join (cmd ))
122- try :
123- res = subprocess .run (cmd , check = False , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
124- if res .returncode != 0 :
125- print ("Compilation failed:" , res .stderr .strip (), file = sys .stderr )
126- return None
127- # ensure executable bit
128- out_path .chmod (0o755 )
129- return out_path
130- except Exception as e :
131- print ("Compilation error:" , e , file = sys .stderr )
132- return None
99+ print ("Compiling arch:" , " " .join (cmd ), file = sys .stderr )
100+ res = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
101+ if res .returncode != 0 :
102+ print ("Compilation of build/arch failed." , file = sys .stderr )
103+ print ("=== stdout ===" , file = sys .stderr )
104+ print (res .stdout , file = sys .stderr )
105+ print ("=== stderr ===" , file = sys .stderr )
106+ print (res .stderr , file = sys .stderr )
107+ raise RuntimeError ("compilation failed for build/arch" )
108+ out_path .chmod (0o755 )
109+ return out_path
133110
134111def call_arch (arch_path : Path , src : Path , out_pkg : Path ) -> int :
135112 cmd = [str (arch_path ), "pack" , str (out_pkg ), str (src )]
136- print ("Running arch:" , " " .join (cmd ))
113+ print ("Running arch:" , " " .join (cmd ), file = sys . stderr )
137114 try :
138- res = subprocess .run (cmd , check = False )
115+ res = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
116+ # print stdout/stderr for CI visibility
117+ if res .stdout :
118+ print ("arch stdout:" , res .stdout , file = sys .stderr )
119+ if res .stderr :
120+ print ("arch stderr:" , res .stderr , file = sys .stderr )
139121 return res .returncode
140122 except Exception as e :
141123 print ("arch invocation failed:" , e , file = sys .stderr )
142124 return 1
143125
144- # -------- tar fallback creation --------
145-
146- def create_tar_fallback (src : Path , out_pkg : Path ) -> int :
147- tmpdir = tempfile .mkdtemp (prefix = "create_pkg_tmp_" )
148- try :
149- tmp_tar = Path (tmpdir ) / "archive.tar"
150- # Use a shell pipeline to create deterministic tar. Keep it simple and rely on GNU tar and gzip.
151- # Build a sorted list of files and feed to tar with --null -T - and deterministic options.
152- # The --transform 's|^\./||' removes leading ./ entries so archive root contains package root contents.
153- shell_cmd = (
154- f"cd { str (src )!r} && "
155- "find . -print0 | LC_ALL=C sort -z | "
156- f"tar --null -T - --numeric-owner --owner=0 --group=0 --mtime='UTC 1970-01-01' -cf { str (tmp_tar )!r} --transform 's|^\\ ./||'"
157- )
158- res = subprocess .run (shell_cmd , shell = True )
159- if res .returncode != 0 :
160- print ("tar creation failed" , file = sys .stderr )
161- return res .returncode
162- tmp_gz = Path (tmpdir ) / "archive.tar.gz"
163- gz_cmd = ["gzip" , "-9" , "-n" , "-c" , str (tmp_tar )]
164- with tmp_gz .open ("wb" ) as outf :
165- proc = subprocess .Popen (gz_cmd , stdout = outf )
166- proc .wait ()
167- if proc .returncode != 0 :
168- print ("gzip failed" , file = sys .stderr )
169- return proc .returncode
170- ensure_parent (out_pkg )
171- shutil .move (str (tmp_gz ), str (out_pkg ))
172- return 0
173- finally :
174- try :
175- shutil .rmtree (tmpdir )
176- except Exception :
177- pass
178-
179126# -------- main flow --------
180127
181128def main (argv ):
182- ap = argparse .ArgumentParser (description = "Create package .pkg from source tree, auto-building arch if needed " )
183- ap .add_argument ("src_dir" , help = "source directory: docs /src/<name>/<ver>" )
129+ ap = argparse .ArgumentParser (description = "Create package .pkg from source tree; always use build/arch " )
130+ ap .add_argument ("src_dir" , help = "source directory: pkgs /src/<name>/<ver>" )
184131 ap .add_argument ("--out-root" , default = "pkgs" , help = "output root (default: pkgs)" )
185132 ap .add_argument ("--keep-temp" , action = "store_true" , help = "keep temporary files on error" )
186- ap .add_argument ("--arch-cmd" , default = None , help = "custom arch command path (optional)" )
187133 args = ap .parse_args (argv )
188134
189135 src = Path (args .src_dir ).resolve ()
@@ -212,39 +158,30 @@ def main(argv):
212158 out_pkg_dir .mkdir (parents = True , exist_ok = True )
213159 out_pkg = out_pkg_dir / f"{ name } -{ version } .pkg"
214160
215- # determine arch binary
216- arch_path = try_find_arch (args .arch_cmd )
217- if not arch_path :
218- print ("arch binary not found. Attempting to build from src/arch.c ..." )
219- built = attempt_build_arch (Path ("src" ))
220- if built :
221- arch_path = built
222- print ("Built arch at" , arch_path )
223- else :
224- print ("Could not build arch; will use tar fallback" , file = sys .stderr )
161+ # ensure parent dir exists
162+ ensure_parent (out_pkg )
225163
226- # try arch if available
227- if arch_path :
228- rc = call_arch (arch_path , src , out_pkg )
229- if rc == 0 and out_pkg .is_file ():
230- print ("arch succeeded, produced:" , out_pkg )
231- else :
232- print (f"arch returned code { rc } ; falling back to deterministic tar" , file = sys .stderr )
233- rc2 = create_tar_fallback (src , out_pkg )
234- if rc2 != 0 :
235- print ("Failed to produce archive via fallback (code {})" .format (rc2 ), file = sys .stderr )
236- return 1
237- print ("Created archive via tar fallback:" , out_pkg )
238- else :
239- rc2 = create_tar_fallback (src , out_pkg )
240- if rc2 != 0 :
241- print ("Failed to produce archive via fallback (code {})" .format (rc2 ), file = sys .stderr )
242- return 1
243- print ("Created archive via tar fallback:" , out_pkg )
164+ # Always use build/arch. Try to find or compile it.
165+ try :
166+ arch_path = arch_executable ()
167+ except Exception as e :
168+ print ("ERROR: could not obtain build/arch:" , e , file = sys .stderr )
169+ return 3
170+
171+ rc = call_arch (arch_path , src , out_pkg )
172+ if rc != 0 or not out_pkg .is_file ():
173+ print (f"ERROR: build/arch failed (rc={ rc } ) or did not produce { out_pkg } " , file = sys .stderr )
174+ return 4
175+
176+ print ("arch succeeded, produced:" , out_pkg , file = sys .stderr )
244177
245178 # compute sha256
246- sha = compute_sha256 (out_pkg )
247- print ("SHA256:" , sha )
179+ try :
180+ sha = compute_sha256 (out_pkg )
181+ print ("SHA256:" , sha )
182+ except Exception as e :
183+ print ("ERROR computing SHA256:" , e , file = sys .stderr )
184+ return 5
248185
249186 # update manifest archive_sha256
250187 try :
0 commit comments