@invarn/cibuild 2.4.2 → 2.4.4

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.
@@ -52,12 +52,6 @@ export interface TrimDims {
52
52
  export interface TrimHelperInvocation {
53
53
  cmd: string;
54
54
  args: string[];
55
- /** Child environment override. Set only for the pkg self-exec, where the
56
- * node-invoke sentinel (PKG_EXECPATH) must be stripped so the re-executed ci
57
- * binary runs its baked cli entrypoint (dispatching `fidelity-trim`) instead
58
- * of re-entering Node-emulation and treating it as a script path. Absent for
59
- * the other homes, which spawn with the inherited environment unchanged. */
60
- env?: Record<string, string>;
61
55
  }
62
56
  /**
63
57
  * The platform-neutral helpers embedded in the trim-stage script.
@@ -79,14 +73,18 @@ export interface PostProcessorInternals {
79
73
  /**
80
74
  * Evaluates the embedded trim-stage source and returns its pure helpers, so
81
75
  * unit tests exercise exactly the code that ships inside the runtime script
82
- * (mirrors getRenderScriptInternals in ui-fidelity-render.ts). `cp` and
83
- * `process` are injectable so the invocation/spawn seams are testable; the
84
- * other trim-stage functions reference runtime globals (fs, path, ...) only
85
- * when called, so defining them here is harmless.
76
+ * (mirrors getRenderScriptInternals in ui-fidelity-render.ts). `cp`, `process`,
77
+ * `fs`, `os` and `path` are injectable so the invocation/spawn/copy seams are
78
+ * testable; at runtime the stage is spliced into the generated script where
79
+ * those names are `require`d globals. Uninjected deps only matter for branches
80
+ * that dereference them (the pkg copy path), so existing callers can omit them.
86
81
  */
87
82
  export declare function getPostProcessorInternals(overrides?: {
88
83
  cp?: unknown;
89
84
  processRef?: unknown;
85
+ fs?: unknown;
86
+ os?: unknown;
87
+ path?: unknown;
90
88
  }): PostProcessorInternals;
91
89
  /**
92
90
  * Resolves the on-disk JS entrypoint of this cibuild installation's CLI
@@ -1 +1 @@
1
- {"version":3,"file":"render-post-processor.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/render-post-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MA6HtC,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5C,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf;;;;iFAI6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAAC;IAC3D,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1F;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,CAAC,EAAE;IACpD,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,sBAAsB,CASzB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAqBhD"}
1
+ {"version":3,"file":"render-post-processor.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/render-post-processor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,wBAAwB,EAAE,MAuJtC,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE5C,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAAC;IAC3D,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,CAAC,EAAE;IACpD,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,GAAG,sBAAsB,CAkBzB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAqBhD"}
@@ -72,14 +72,44 @@ function parseTrimDims(stdout) {
72
72
  return null;
73
73
  }
74
74
 
75
+ // Lazily copy the pkg ci binary to a scratch path so the trim can invoke it
76
+ // WITHOUT re-executing process.execPath. Under pkg the generated render script
77
+ // runs as an app-mode child of the ci binary, so process.execPath IS the ci
78
+ // binary -- but pkg refuses to let a binary re-invoke ITSELF as a subcommand:
79
+ // spawning process.execPath forces the child into Node-emulation, where
80
+ // 'fidelity-trim' is treated as a main-module script path ("Cannot find module
81
+ // '<cwd>/fidelity-trim'") and the subcommand dispatch never runs, so the trim
82
+ // silently no-ops and untrimmed renders ship. A COPY of the binary at a
83
+ // different path is NOT seen as a self-exec, so it runs the baked cli entrypoint
84
+ // normally (with the bundled pngjs). Copied once per run; returns null if the
85
+ // copy can't be made, and the caller then degrades to a best-effort no-op trim.
86
+ // Verified on the runner: a copy-spawn dispatches and emits TRIMDIMS, while an
87
+ // execPath self-exec crashes with the module-not-found above.
88
+ var ciTrimBinCopy;
89
+ function ciBinaryCopyForTrim() {
90
+ if (typeof ciTrimBinCopy !== 'undefined') return ciTrimBinCopy;
91
+ try {
92
+ var dest = path.join(os.tmpdir(), 'ci-fidelity-trim-bin');
93
+ if (!fs.existsSync(dest)) {
94
+ fs.copyFileSync(process.execPath, dest);
95
+ fs.chmodSync(dest, 493); // 0o755
96
+ }
97
+ ciTrimBinCopy = dest;
98
+ } catch (copyErr) {
99
+ ciTrimBinCopy = null;
100
+ }
101
+ return ciTrimBinCopy;
102
+ }
103
+
75
104
  // Resolve how to reach the ci CLI's fidelity-trim helper from inside this
76
105
  // generated script. Three homes, in order:
77
106
  // 1. CIBUILD_CLI_JS -- an explicit JS entrypoint (preview harnesses, tests),
78
107
  // run under the same node executing this script.
79
- // 2. The pkg-built ci binary re-executing itself -- the production runner
80
- // path, where this script runs under the pkg bootstrap and
81
- // process.execPath IS the ci binary.
82
- // 3. A ci on PATH (brew/npm installs).
108
+ // 2. Under pkg (the production runner path) process.execPath IS the ci binary,
109
+ // but pkg won't let it re-invoke itself as a subcommand, so spawn a COPY of
110
+ // it (see ciBinaryCopyForTrim). Fall back to the (best-effort) self-exec if
111
+ // the copy can't be made.
112
+ // 3. A ci on PATH (brew/npm installs) when not running under pkg.
83
113
  function trimHelperInvocation(args) {
84
114
  if (process.env.CIBUILD_CLI_JS) {
85
115
  return {
@@ -88,22 +118,11 @@ function trimHelperInvocation(args) {
88
118
  };
89
119
  }
90
120
  if (process.pkg) {
91
- // The runner spawns this script as 'node <script>', which the pkg binary
92
- // intercepts and runs in Node-emulation mode -- so process.env carries
93
- // pkg's node-invoke sentinel (PKG_EXECPATH). Re-execing process.execPath
94
- // with it still set makes the child ci binary ALSO enter Node-emulation and
95
- // treat 'fidelity-trim' as a main-module script path (Cannot find module
96
- // '<cwd>/fidelity-trim'), never reaching the subcommand dispatch -- so the
97
- // trim silently fails and untrimmed renders ship. Strip PKG_EXECPATH from
98
- // the child env so the re-exec runs the baked cli entrypoint, which routes
99
- // 'fidelity-trim' to the in-binary helper (with bundled pngjs).
100
- var invocation = { cmd: process.execPath, args: ['fidelity-trim'].concat(args) };
101
- if (process.env.PKG_EXECPATH !== undefined) {
102
- var appEnv = Object.assign({}, process.env);
103
- delete appEnv.PKG_EXECPATH;
104
- invocation.env = appEnv;
105
- }
106
- return invocation;
121
+ var copy = ciBinaryCopyForTrim();
122
+ return {
123
+ cmd: copy === null ? process.execPath : copy,
124
+ args: ['fidelity-trim'].concat(args),
125
+ };
107
126
  }
108
127
  return { cmd: 'ci', args: ['fidelity-trim'].concat(args) };
109
128
  }
@@ -112,9 +131,16 @@ function trimHelperInvocation(args) {
112
131
  // stdout line, and exit codes are the retired CoreGraphics helper's contract).
113
132
  function runTrimHelper(args) {
114
133
  var invocation = trimHelperInvocation(args);
115
- var options = { encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024 };
116
- if (invocation.env !== undefined) options.env = invocation.env;
117
- var result = cp.spawnSync(invocation.cmd, invocation.args, options);
134
+ // Strip pkg's node-invoke sentinel (PKG_EXECPATH) from the child env as a
135
+ // belt-and-suspenders alongside the copy-binary indirection above, so the
136
+ // spawned ci runs its baked cli entrypoint and dispatches the subcommand.
137
+ var childEnv = Object.assign({}, process.env);
138
+ delete childEnv.PKG_EXECPATH;
139
+ var result = cp.spawnSync(invocation.cmd, invocation.args, {
140
+ encoding: 'utf-8',
141
+ maxBuffer: 16 * 1024 * 1024,
142
+ env: childEnv,
143
+ });
118
144
  if (result.stdout) process.stdout.write(result.stdout);
119
145
  if (result.stderr) process.stderr.write(result.stderr);
120
146
  if (result.error) {
@@ -170,16 +196,17 @@ function trimRenderedImage(entry, renderedPath) {
170
196
  /**
171
197
  * Evaluates the embedded trim-stage source and returns its pure helpers, so
172
198
  * unit tests exercise exactly the code that ships inside the runtime script
173
- * (mirrors getRenderScriptInternals in ui-fidelity-render.ts). `cp` and
174
- * `process` are injectable so the invocation/spawn seams are testable; the
175
- * other trim-stage functions reference runtime globals (fs, path, ...) only
176
- * when called, so defining them here is harmless.
199
+ * (mirrors getRenderScriptInternals in ui-fidelity-render.ts). `cp`, `process`,
200
+ * `fs`, `os` and `path` are injectable so the invocation/spawn/copy seams are
201
+ * testable; at runtime the stage is spliced into the generated script where
202
+ * those names are `require`d globals. Uninjected deps only matter for branches
203
+ * that dereference them (the pkg copy path), so existing callers can omit them.
177
204
  */
178
205
  export function getPostProcessorInternals(overrides) {
179
- const factory = new Function('cp', 'process', RENDER_SCRIPT_TRIM_STAGE +
206
+ const factory = new Function('cp', 'process', 'fs', 'os', 'path', RENDER_SCRIPT_TRIM_STAGE +
180
207
  '\nreturn { parseTrimDims: parseTrimDims, roundTwo: roundTwo, ' +
181
208
  'trimHelperInvocation: trimHelperInvocation, runTrimHelper: runTrimHelper };');
182
- return factory(overrides?.cp, overrides?.processRef ?? process);
209
+ return factory(overrides?.cp, overrides?.processRef ?? process, overrides?.fs, overrides?.os, overrides?.path);
183
210
  }
184
211
  /**
185
212
  * Resolves the on-disk JS entrypoint of this cibuild installation's CLI
@@ -76,18 +76,52 @@ describe('trim stage — helper invocation resolution', () => {
76
76
  args: ['/opt/cibuild/cli.cjs', 'fidelity-trim', 'in.png', 'out.png'],
77
77
  });
78
78
  });
79
- test('under the pkg bootstrap the ci binary re-executes itself', () => {
80
- const { trimHelperInvocation } = internalsWith({
81
- env: {},
82
- pkg: {},
83
- execPath: '/opt/homebrew/bin/ci',
79
+ // Under pkg (the production runner path — process.pkg IS set, verified on the
80
+ // runner), process.execPath IS the ci binary, and pkg REFUSES to let a binary
81
+ // re-invoke itself as a subcommand: spawning process.execPath forces the child
82
+ // into Node-emulation, where 'fidelity-trim' becomes a main-module script path
83
+ // ("Cannot find module '<cwd>/fidelity-trim'") and the dispatch never runs —
84
+ // the trim silently no-ops and untrimmed renders ship. A COPY of the binary at
85
+ // a different path is not a self-exec, so it runs the baked cli entrypoint.
86
+ // (Verified on the runner: copy-spawn dispatches + emits TRIMDIMS.)
87
+ function pkgInternals(opts) {
88
+ const copyCalls = [];
89
+ const fakeFs = {
90
+ existsSync: () => false,
91
+ copyFileSync: (src, dest) => {
92
+ copyCalls.push([src, dest]);
93
+ if (opts?.copyThrows)
94
+ throw new Error('EACCES');
95
+ },
96
+ chmodSync: () => { },
97
+ };
98
+ const fakeOs = { tmpdir: () => '/scratch' };
99
+ const fakePath = { join: (...parts) => parts.join('/') };
100
+ const internals = getPostProcessorInternals({
101
+ processRef: { env: {}, pkg: {}, execPath: '/opt/homebrew/bin/ci' },
102
+ fs: fakeFs,
103
+ os: fakeOs,
104
+ path: fakePath,
84
105
  });
85
- expect(trimHelperInvocation(['in.png', 'out.png', 'aligned.png', 'ref.png'])).toEqual({
86
- cmd: '/opt/homebrew/bin/ci',
106
+ return { internals, copyCalls };
107
+ }
108
+ test('under pkg it spawns a COPY of the binary (not process.execPath)', () => {
109
+ const { internals, copyCalls } = pkgInternals();
110
+ expect(internals.trimHelperInvocation(['in.png', 'out.png', 'aligned.png', 'ref.png'])).toEqual({
111
+ cmd: '/scratch/ci-fidelity-trim-bin',
87
112
  args: ['fidelity-trim', 'in.png', 'out.png', 'aligned.png', 'ref.png'],
88
113
  });
114
+ // The copy is made once, from process.execPath.
115
+ expect(copyCalls).toEqual([['/opt/homebrew/bin/ci', '/scratch/ci-fidelity-trim-bin']]);
116
+ });
117
+ test('under pkg it degrades to process.execPath when the binary copy fails', () => {
118
+ const { internals } = pkgInternals({ copyThrows: true });
119
+ expect(internals.trimHelperInvocation(['in.png', 'out.png'])).toEqual({
120
+ cmd: '/opt/homebrew/bin/ci',
121
+ args: ['fidelity-trim', 'in.png', 'out.png'],
122
+ });
89
123
  });
90
- test('falls back to a ci on PATH', () => {
124
+ test('falls back to a ci on PATH when not running under pkg', () => {
91
125
  const { trimHelperInvocation } = internalsWith({
92
126
  env: {},
93
127
  execPath: '/usr/local/bin/node',
@@ -97,31 +131,11 @@ describe('trim stage — helper invocation resolution', () => {
97
131
  args: ['fidelity-trim', 'in.png', 'out.png'],
98
132
  });
99
133
  });
100
- // The production runner spawns the render step as `node <script>`, which the
101
- // pkg-built ci binary intercepts and runs in Node-emulation mode so the
102
- // running script's process.env carries pkg's node-invoke sentinel
103
- // (PKG_EXECPATH). If the trim re-exec inherits it, the child ci binary ALSO
104
- // enters Node-emulation and treats 'fidelity-trim' as a main-module script
105
- // path ("Cannot find module '<cwd>/fidelity-trim'") instead of dispatching
106
- // the subcommand — the exact failure observed in production, which silently
107
- // ships untrimmed renders. The re-exec must clear PKG_EXECPATH so the binary
108
- // runs its baked cli entrypoint.
109
- test('under pkg node-emulation the re-exec clears PKG_EXECPATH so the binary runs its app entrypoint', () => {
110
- const { trimHelperInvocation } = internalsWith({
111
- env: { PKG_EXECPATH: 'PKG_INVOKE_NODEJS', PATH: '/usr/bin:/bin' },
112
- pkg: {},
113
- execPath: '/opt/homebrew/bin/ci',
114
- });
115
- const invocation = trimHelperInvocation(['in.png', 'out.png']);
116
- expect(invocation.cmd).toBe('/opt/homebrew/bin/ci');
117
- expect(invocation.args).toEqual(['fidelity-trim', 'in.png', 'out.png']);
118
- // A child env is supplied with the node-invoke sentinel removed, but the
119
- // rest of the environment (PATH, etc.) preserved.
120
- expect(invocation.env).toBeDefined();
121
- expect(invocation.env?.PKG_EXECPATH).toBeUndefined();
122
- expect(invocation.env?.PATH).toBe('/usr/bin:/bin');
123
- });
124
- test('runTrimHelper spawns the pkg re-exec with a PKG_EXECPATH-cleared env', () => {
134
+ // runTrimHelper strips pkg's node-invoke sentinel (PKG_EXECPATH) from the child
135
+ // env as belt-and-suspenders alongside the copy-binary indirection, so the
136
+ // spawned ci runs its baked cli entrypoint rather than re-entering
137
+ // Node-emulation. (Exercised here via the non-pkg `ci` home for a simple seam.)
138
+ test('runTrimHelper strips PKG_EXECPATH from the child env', () => {
125
139
  const calls = [];
126
140
  const fakeCp = {
127
141
  spawnSync: (...args) => {
@@ -132,9 +146,9 @@ describe('trim stage — helper invocation resolution', () => {
132
146
  const { runTrimHelper } = getPostProcessorInternals({
133
147
  cp: fakeCp,
134
148
  processRef: {
135
- env: { PKG_EXECPATH: 'PKG_INVOKE_NODEJS', FOO: 'bar' },
136
- pkg: {},
137
- execPath: '/opt/homebrew/bin/ci',
149
+ // Node-emulation shape: the sentinel is set but process.pkg is NOT.
150
+ env: { PKG_EXECPATH: 'PKG_INVOKE_NODEJS', PATH: '/usr/bin:/bin', FOO: 'bar' },
151
+ execPath: '/usr/local/bin/node',
138
152
  stdout: { write: () => true },
139
153
  stderr: { write: () => true },
140
154
  },
@@ -142,9 +156,12 @@ describe('trim stage — helper invocation resolution', () => {
142
156
  runTrimHelper(['in.png', 'out.png']);
143
157
  expect(calls).toHaveLength(1);
144
158
  const [cmd, , options] = calls[0];
145
- expect(cmd).toBe('/opt/homebrew/bin/ci');
159
+ // process.pkg undefined → resolves to `ci` on PATH (the real runner path).
160
+ expect(cmd).toBe('ci');
161
+ // The child env must have the node-invoke sentinel removed, the rest kept.
146
162
  expect(options.env).toBeDefined();
147
163
  expect(options.env?.PKG_EXECPATH).toBeUndefined();
164
+ expect(options.env?.PATH).toBe('/usr/bin:/bin');
148
165
  expect(options.env?.FOO).toBe('bar');
149
166
  });
150
167
  test('runTrimHelper reports a spawn error as status 127 with the message appended', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",