@coderook/cli 0.34.0 → 0.34.1
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +7 -6
- package/dist/cbx/src/core/cbx.js +9 -0
- package/dist/cli/src/build_command.js +131 -7
- package/dist/cli/src/cli.js +11 -5
- package/dist/cli/src/git_remote_bin.js +3 -1
- package/dist/cli/src/node_check.js +26 -0
- package/dist/cli/src/portable_python.js +370 -0
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "coderook",
|
|
3
3
|
"displayName": "CodeRook",
|
|
4
4
|
"description": "Save, browse and restore whole-snapshot versions of a project on CodeRook, with cbx or plain git, from Claude Code.",
|
|
5
|
-
"version": "0.34.
|
|
5
|
+
"version": "0.34.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ACCA Gaming Productions",
|
|
8
8
|
"url": "https://coderook.com"
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ project moved by one is understood by the other.
|
|
|
32
32
|
|
|
33
33
|
## Install
|
|
34
34
|
|
|
35
|
-
Requires Node
|
|
35
|
+
Requires Node 22.15 or later (Node 24 is the safe choice), and nothing else. Older Node lacks the zstd support bundles use, and the CLI says so rather than failing later.
|
|
36
36
|
|
|
37
37
|
```
|
|
38
38
|
npm install --global @coderook/cli
|
|
@@ -446,16 +446,17 @@ cbx build init describe the build in coderook.build.json
|
|
|
446
446
|
```
|
|
447
447
|
|
|
448
448
|
`cbx build` makes the project's executables for Windows, Linux and macOS on
|
|
449
|
-
the machine you are using. Each target is built natively
|
|
450
|
-
|
|
451
|
-
is named, with what
|
|
449
|
+
the machine you are using. Each target is built natively or cross-compiled,
|
|
450
|
+
chosen from what is installed; Docker and WSL are used when present and never
|
|
451
|
+
required for Linux. A target that cannot be built here is named, with what
|
|
452
|
+
would build it.
|
|
452
453
|
|
|
453
454
|
| Toolchain | From any machine | Needs its own system |
|
|
454
455
|
| --- | --- | --- |
|
|
455
456
|
| Go, Bun, Deno, .NET | Windows, Linux, macOS | — |
|
|
456
457
|
| Rust | all three, with cargo-zigbuild | macOS apps using system frameworks |
|
|
457
|
-
| Electron | Windows and Linux
|
|
458
|
-
|
|
|
458
|
+
| Electron | Linux (tar.gz) straight from Windows; Windows and Linux elsewhere with Docker or wine | macOS; AppImage and deb need Linux |
|
|
459
|
+
| Python | Windows, Linux, macOS: the target's own CPython with its wheels, as a tar.gz with a launcher (PyInstaller where it runs) | a dependency with no wheel for that system |
|
|
459
460
|
|
|
460
461
|
The toolchain is recognised from the files present. `coderook.build.json`
|
|
461
462
|
settles it, and takes a command of your own (`{target}`, `{os}`, `{arch}` and
|
package/dist/cbx/src/core/cbx.js
CHANGED
|
@@ -31,6 +31,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
31
31
|
const node_fs_1 = require("node:fs");
|
|
32
32
|
const promises_1 = require("node:fs/promises");
|
|
33
33
|
const node_path_1 = __importDefault(require("node:path"));
|
|
34
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
34
35
|
const safe_path_js_1 = require("../shared/safe_path.js");
|
|
35
36
|
const node_zlib_1 = require("node:zlib");
|
|
36
37
|
const download_js_1 = require("./download.js");
|
|
@@ -411,6 +412,14 @@ async function unpackBundle(source, destination, report) {
|
|
|
411
412
|
throw new Error(`${record.path} did not survive the bundle intact`);
|
|
412
413
|
}
|
|
413
414
|
await (0, promises_1.writeFile)(full, contents);
|
|
415
|
+
/*
|
|
416
|
+
The mode was recorded when the bundle was made and never put back, so
|
|
417
|
+
a script that went in executable came out needing chmod. Only the
|
|
418
|
+
permission bits, and only where the system has them.
|
|
419
|
+
*/
|
|
420
|
+
if (node_process_1.default.platform !== "win32" && record.mode & 0o111) {
|
|
421
|
+
await (0, promises_1.chmod)(full, (record.mode & 0o777) | 0o400);
|
|
422
|
+
}
|
|
414
423
|
written += 1;
|
|
415
424
|
bytes += contents.length;
|
|
416
425
|
}
|
|
@@ -43,6 +43,7 @@ exports.detectTools = detectTools;
|
|
|
43
43
|
exports.describeProject = describeProject;
|
|
44
44
|
exports.wslPath = wslPath;
|
|
45
45
|
exports.planBuild = planBuild;
|
|
46
|
+
exports.markExecutables = markExecutables;
|
|
46
47
|
exports.fileFromTar = fileFromTar;
|
|
47
48
|
exports.outputsOf = outputsOf;
|
|
48
49
|
exports.commandBuild = commandBuild;
|
|
@@ -78,6 +79,7 @@ const node_fs_1 = require("node:fs");
|
|
|
78
79
|
const promises_1 = require("node:fs/promises");
|
|
79
80
|
const node_path_1 = __importDefault(require("node:path"));
|
|
80
81
|
const node_process_1 = __importDefault(require("node:process"));
|
|
82
|
+
const portable_python_js_1 = require("./portable_python.js");
|
|
81
83
|
const ESC = String.fromCharCode(27);
|
|
82
84
|
const colour = node_process_1.default.stdout.isTTY && !node_process_1.default.env.NO_COLOR;
|
|
83
85
|
const paint = (code) => (value) => (colour ? `${ESC}[${code}m${value}${ESC}[0m` : value);
|
|
@@ -156,6 +158,8 @@ async function detectTools(host = thisHost()) {
|
|
|
156
158
|
/* The daemon, not the client: a stopped Docker Desktop answers --version. */
|
|
157
159
|
["docker", answers("docker", ["info", "--format", "{{.ServerVersion}}"], 15000)],
|
|
158
160
|
["pyinstaller", answers("pyinstaller", ["--version"], 15000)],
|
|
161
|
+
["uv", answers("uv", ["--version"])],
|
|
162
|
+
["python", answers(host.os === "windows" ? "python" : "python3", ["-m", "pip", "--version"], 15000)],
|
|
159
163
|
["wine", host.os === "windows" ? Promise.resolve(false) : answers("wine", ["--version"])],
|
|
160
164
|
["node", answers(node_process_1.default.execPath, ["--version"])],
|
|
161
165
|
];
|
|
@@ -210,6 +214,8 @@ async function describeProject(folder) {
|
|
|
210
214
|
preset = "bun";
|
|
211
215
|
else if (spec || (await (0, promises_1.readFile)(node_path_1.default.join(folder, "requirements.txt"), "utf8").catch(() => "")).match(/pyinstaller/i))
|
|
212
216
|
preset = "pyinstaller";
|
|
217
|
+
else if (here("requirements.txt") || here("pyproject.toml") || here("main.py") || here("app.py"))
|
|
218
|
+
preset = "python";
|
|
213
219
|
}
|
|
214
220
|
let name = config.name ?? "";
|
|
215
221
|
let entry = config.entry ?? null;
|
|
@@ -240,7 +246,7 @@ async function describeProject(folder) {
|
|
|
240
246
|
["main.ts", "mod.ts", "index.ts", "src/main.ts", "src/index.ts", "index.js"].find(here) ??
|
|
241
247
|
null;
|
|
242
248
|
}
|
|
243
|
-
if (preset === "pyinstaller") {
|
|
249
|
+
if (preset === "pyinstaller" || preset === "python") {
|
|
244
250
|
entry ??= spec ?? ["main.py", "app.py", "__main__.py", "src/main.py"].find(here) ?? null;
|
|
245
251
|
}
|
|
246
252
|
if (!name && typeof pkg?.name === "string")
|
|
@@ -327,7 +333,21 @@ function planBuild(project, host, tools, targets) {
|
|
|
327
333
|
const sameOs = host.os === os_;
|
|
328
334
|
const file = `${project.name}-${target}${exe(target)}`;
|
|
329
335
|
const pre = before ? [{ shell: before }] : [];
|
|
330
|
-
|
|
336
|
+
/* Found by asking the PATH, so a tool installed but not on it reads as absent: say both. */
|
|
337
|
+
const missing = (tool, why = "") => ({ target, ok: false, why: `needs ${tool}${why}, on the PATH this command sees` });
|
|
338
|
+
/*
|
|
339
|
+
Python for any system from any system: the target's own CPython, its
|
|
340
|
+
wheels and the program, in one archive. Needs uv, or Python with pip to
|
|
341
|
+
fetch uv; a dependency with no wheel for the target is the one thing it
|
|
342
|
+
cannot do, and it says which.
|
|
343
|
+
*/
|
|
344
|
+
const portable = () => tools.uv || tools.python
|
|
345
|
+
? {
|
|
346
|
+
target, ok: true, route: native ? "native" : "cross", out,
|
|
347
|
+
how: `portable CPython for ${target} + its wheels (uv), as a tar.gz with a launcher`,
|
|
348
|
+
steps: [...pre, { portablePython: { target, out: outAbs } }],
|
|
349
|
+
}
|
|
350
|
+
: null;
|
|
331
351
|
switch (project.preset) {
|
|
332
352
|
case "go": {
|
|
333
353
|
if (!tools.go)
|
|
@@ -471,6 +491,27 @@ function planBuild(project, host, tools, targets) {
|
|
|
471
491
|
steps: [...pre, { program: node_process_1.default.execPath, args: [node_path_1.default.join(project.folder, "node_modules", "electron-builder", "cli.js"), ...builder.slice(1), outputFlag(out)] }],
|
|
472
492
|
};
|
|
473
493
|
}
|
|
494
|
+
/*
|
|
495
|
+
Linux from Windows needs no container. electron-builder packages a
|
|
496
|
+
Linux app on Windows by itself; what it cannot do is mark anything
|
|
497
|
+
executable, because Windows has no such bit, so every program in
|
|
498
|
+
the archive arrives as a plain file. The archive is fixed up after.
|
|
499
|
+
A tar.gz only: AppImage needs symlinks and Linux tools, deb needs fpm.
|
|
500
|
+
*/
|
|
501
|
+
if (os_ === "linux" && host.os === "windows") {
|
|
502
|
+
return {
|
|
503
|
+
target, ok: true, route: "cross", out,
|
|
504
|
+
how: `electron-builder --linux tar.gz --${arch} on Windows, executables marked afterwards (AppImage and deb need Linux)`,
|
|
505
|
+
steps: [
|
|
506
|
+
...pre,
|
|
507
|
+
{
|
|
508
|
+
program: node_process_1.default.execPath,
|
|
509
|
+
args: [node_path_1.default.join(project.folder, "node_modules", "electron-builder", "cli.js"), "--linux", "tar.gz", `--${arch}`, "--publish", "never", ...extra, outputFlag(out)],
|
|
510
|
+
},
|
|
511
|
+
{ fixTarModes: outAbs },
|
|
512
|
+
],
|
|
513
|
+
};
|
|
514
|
+
}
|
|
474
515
|
const nativeOk = sameOs || (os_ === "windows" && host.os !== "windows" && tools.wine);
|
|
475
516
|
if (nativeOk) {
|
|
476
517
|
return {
|
|
@@ -507,7 +548,7 @@ function planBuild(project, host, tools, targets) {
|
|
|
507
548
|
: ["--noconfirm", "--onefile", "--name", `${project.name}-${target}`, "--distpath", dist, "--workpath", workDir, "--specpath", specDir, ...extra, project.entry];
|
|
508
549
|
if (native) {
|
|
509
550
|
if (!tools.pyinstaller)
|
|
510
|
-
return missing("PyInstaller (pip install pyinstaller)");
|
|
551
|
+
return portable() ?? missing("PyInstaller (pip install pyinstaller)");
|
|
511
552
|
return {
|
|
512
553
|
target, ok: true, route: "native", out, how: "pyinstaller",
|
|
513
554
|
steps: [...pre, { program: "pyinstaller", args: flags(outAbs, node_path_1.default.join(project.folder, work), node_path_1.default.join(project.folder, "build", "coderook")) }],
|
|
@@ -531,9 +572,14 @@ function planBuild(project, host, tools, targets) {
|
|
|
531
572
|
steps: [{ program: "wsl", args: ["-e", "sh", "-lc", inner] }],
|
|
532
573
|
};
|
|
533
574
|
}
|
|
534
|
-
return { target, ok: false, why: `PyInstaller builds only for the system it runs on; needs Docker running, or WSL with PyInstaller, or ${NEEDS("linux")}` };
|
|
575
|
+
return portable() ?? { target, ok: false, why: `PyInstaller builds only for the system it runs on; needs Docker running, or WSL with PyInstaller, or ${NEEDS("linux")}` };
|
|
535
576
|
}
|
|
536
|
-
return { target, ok: false, why: `PyInstaller builds only for the system it runs on; ${NEEDS(os_)}` };
|
|
577
|
+
return portable() ?? { target, ok: false, why: `PyInstaller builds only for the system it runs on; ${NEEDS(os_)}` };
|
|
578
|
+
}
|
|
579
|
+
case "python": {
|
|
580
|
+
if (!project.entry)
|
|
581
|
+
return { target, ok: false, why: `no entry point; set "entry" in ${CONFIG_FILE}` };
|
|
582
|
+
return portable() ?? missing("uv, or Python with pip (https://docs.astral.sh/uv)");
|
|
537
583
|
}
|
|
538
584
|
case "custom": {
|
|
539
585
|
if (!project.config.command)
|
|
@@ -570,6 +616,55 @@ function output(program, args) {
|
|
|
570
616
|
child.on("exit", (code) => done(code === 0 ? text.trim() : null));
|
|
571
617
|
});
|
|
572
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Mark the programs in a tar archive executable: Linux ELF binaries and
|
|
621
|
+
* scripts with a `#!` line. Each entry's mode is rewritten in its header and
|
|
622
|
+
* the header checksum recomputed; nothing else in the archive moves.
|
|
623
|
+
*/
|
|
624
|
+
function markExecutables(archive) {
|
|
625
|
+
const out = Buffer.from(archive);
|
|
626
|
+
const marked = [];
|
|
627
|
+
let at = 0;
|
|
628
|
+
while (at + 512 <= out.length) {
|
|
629
|
+
const header = out.subarray(at, at + 512);
|
|
630
|
+
if (header.every((byte) => byte === 0))
|
|
631
|
+
break;
|
|
632
|
+
const field = (start, length) => header.subarray(start, start + length).toString("utf8").replace(/\0.*$/s, "");
|
|
633
|
+
const size = parseInt(field(124, 12).trim() || "0", 8);
|
|
634
|
+
const type = String.fromCharCode(header[156] ?? 48);
|
|
635
|
+
const body = out.subarray(at + 512, at + 512 + Math.min(size, 4));
|
|
636
|
+
const program = (body[0] === 0x7f && body.toString("latin1", 1, 4) === "ELF") || body.toString("latin1", 0, 2) === "#!";
|
|
637
|
+
if ((type === "0" || type === "\0") && program) {
|
|
638
|
+
const mode = parseInt(field(100, 8).trim() || "0", 8);
|
|
639
|
+
const wanted = mode | 0o755;
|
|
640
|
+
if (wanted !== mode) {
|
|
641
|
+
header.write(`${wanted.toString(8).padStart(7, "0")}\0`, 100, 8, "latin1");
|
|
642
|
+
header.fill(0x20, 148, 156);
|
|
643
|
+
let sum = 0;
|
|
644
|
+
for (const byte of header)
|
|
645
|
+
sum += byte;
|
|
646
|
+
header.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "latin1");
|
|
647
|
+
const prefix = field(345, 155);
|
|
648
|
+
marked.push(prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
at += 512 + Math.ceil(size / 512) * 512;
|
|
652
|
+
}
|
|
653
|
+
return { archive: out, marked };
|
|
654
|
+
}
|
|
655
|
+
async function fixTarModes(folder) {
|
|
656
|
+
for (const name of await (0, promises_1.readdir)(folder).catch(() => [])) {
|
|
657
|
+
if (!name.endsWith(".tar.gz"))
|
|
658
|
+
continue;
|
|
659
|
+
const file = node_path_1.default.join(folder, name);
|
|
660
|
+
const { archive, marked } = markExecutables((0, node_zlib_1.gunzipSync)(await (0, promises_1.readFile)(file)));
|
|
661
|
+
if (!marked.length)
|
|
662
|
+
continue;
|
|
663
|
+
await (0, promises_1.writeFile)(file, (0, node_zlib_1.gzipSync)(archive, { level: 9 }));
|
|
664
|
+
console.log(dim(`Marked ${marked.length} program${marked.length === 1 ? "" : "s"} executable in ${name}.`));
|
|
665
|
+
}
|
|
666
|
+
return 0;
|
|
667
|
+
}
|
|
573
668
|
/** The one file at `name` in a tar archive, read without unpacking the rest. */
|
|
574
669
|
function fileFromTar(archive, name) {
|
|
575
670
|
let at = 0;
|
|
@@ -619,9 +714,37 @@ async function fetchBunRuntime(target, to) {
|
|
|
619
714
|
return 1;
|
|
620
715
|
}
|
|
621
716
|
}
|
|
717
|
+
/** The project being built, for the steps that need more than a command line. */
|
|
718
|
+
let building = null;
|
|
719
|
+
async function runPortable(step) {
|
|
720
|
+
if (!building?.entry)
|
|
721
|
+
return 1;
|
|
722
|
+
try {
|
|
723
|
+
await (0, portable_python_js_1.buildPortablePython)({
|
|
724
|
+
folder: building.folder,
|
|
725
|
+
target: step.target,
|
|
726
|
+
name: building.name,
|
|
727
|
+
entry: building.entry,
|
|
728
|
+
...(building.config.python ? { python: building.config.python } : {}),
|
|
729
|
+
...(building.config.gui ? { gui: true } : {}),
|
|
730
|
+
outFolder: step.out,
|
|
731
|
+
cache: cacheDirectory(),
|
|
732
|
+
say: (text) => console.log(dim(text)),
|
|
733
|
+
});
|
|
734
|
+
return 0;
|
|
735
|
+
}
|
|
736
|
+
catch (error) {
|
|
737
|
+
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
738
|
+
return 1;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
622
741
|
function run(step, cwd) {
|
|
742
|
+
if ("portablePython" in step)
|
|
743
|
+
return runPortable(step.portablePython);
|
|
623
744
|
if ("bunRuntime" in step)
|
|
624
745
|
return fetchBunRuntime(step.bunRuntime.target, step.bunRuntime.to);
|
|
746
|
+
if ("fixTarModes" in step)
|
|
747
|
+
return fixTarModes(step.fixTarModes);
|
|
625
748
|
if ("copy" in step) {
|
|
626
749
|
return (async () => {
|
|
627
750
|
try {
|
|
@@ -688,8 +811,8 @@ async function commandBuild(parsed) {
|
|
|
688
811
|
project = { ...project, preset: presetFlag };
|
|
689
812
|
if (!project.preset) {
|
|
690
813
|
console.error(red("No toolchain recognised in this folder."));
|
|
691
|
-
console.error(dim(` Looked for go.mod, Cargo.toml, a .csproj, electron-builder, deno.json, a Bun lockfile
|
|
692
|
-
console.error(dim(` Say which with ${accent("--preset <go|bun|deno|rust|dotnet|electron|pyinstaller>")}, or describe it in ${CONFIG_FILE} (${accent("cbx build init")}).`));
|
|
814
|
+
console.error(dim(` Looked for go.mod, Cargo.toml, a .csproj, electron-builder, deno.json, a Bun lockfile, a PyInstaller .spec and Python files.`));
|
|
815
|
+
console.error(dim(` Say which with ${accent("--preset <go|bun|deno|rust|dotnet|electron|pyinstaller|python>")}, or describe it in ${CONFIG_FILE} (${accent("cbx build init")}).`));
|
|
693
816
|
return 1;
|
|
694
817
|
}
|
|
695
818
|
let targets;
|
|
@@ -705,6 +828,7 @@ async function commandBuild(parsed) {
|
|
|
705
828
|
console.error(red(error instanceof Error ? error.message : String(error)));
|
|
706
829
|
return 1;
|
|
707
830
|
}
|
|
831
|
+
building = project;
|
|
708
832
|
const host = thisHost();
|
|
709
833
|
console.log(dim(`Checking what this ${host.os} ${host.arch} machine can build with…`));
|
|
710
834
|
const tools = await detectTools(host);
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ const progress_js_1 = require("./progress.js");
|
|
|
29
29
|
const publish_js_1 = require("./publish.js");
|
|
30
30
|
const attach_command_js_1 = require("./attach_command.js");
|
|
31
31
|
const build_command_js_1 = require("./build_command.js");
|
|
32
|
+
const node_check_js_1 = require("./node_check.js");
|
|
32
33
|
const pages_command_js_1 = require("./pages_command.js");
|
|
33
34
|
const project_commands_js_1 = require("./project_commands.js");
|
|
34
35
|
const track_commands_js_1 = require("./track_commands.js");
|
|
@@ -371,8 +372,7 @@ async function commandUnlink(parsed) {
|
|
|
371
372
|
console.log("This folder was not linked to a project.");
|
|
372
373
|
return 0;
|
|
373
374
|
}
|
|
374
|
-
console.log(`
|
|
375
|
-
(link?.slug ? dim(` from ${link.slug}`) : "") +
|
|
375
|
+
console.log((link?.slug ? `Unlinked this folder from ${bold(link.slug)}` : `Unlinked ${bold(node_path_1.default.basename(folder))}`) +
|
|
376
376
|
".");
|
|
377
377
|
console.log(dim("Nothing was deleted. The next submit from here asks which project it is."));
|
|
378
378
|
return 0;
|
|
@@ -3309,8 +3309,9 @@ SPECS.push({
|
|
|
3309
3309
|
usage: "build [folder] [--target win,linux,mac]",
|
|
3310
3310
|
detail: "Builds the project in this folder for each target, using whatever this\n" +
|
|
3311
3311
|
"machine has: Go, Bun, Deno and .NET build every platform from any one;\n" +
|
|
3312
|
-
"Rust does with cargo-zigbuild; Electron builds
|
|
3313
|
-
"
|
|
3312
|
+
"Rust does with cargo-zigbuild; Electron builds Linux from Windows with\n" +
|
|
3313
|
+
"nothing extra; Python ships the target's own interpreter and wheels.\n" +
|
|
3314
|
+
"Linux never needs Docker. A target this machine\n" +
|
|
3314
3315
|
"cannot build is named, with what would build it. Nothing is uploaded\n" +
|
|
3315
3316
|
"unless --ship is given. Output goes to .coderook/build/<target>/.\n\n" +
|
|
3316
3317
|
"The toolchain is recognised from the files here. To fix it, or to give\n" +
|
|
@@ -3319,7 +3320,7 @@ SPECS.push({
|
|
|
3319
3320
|
options: [
|
|
3320
3321
|
{ flags: "-t, --target <list>", description: "windows-x64, linux-arm64, macos-arm64 … or win, linux, mac, all" },
|
|
3321
3322
|
{ flags: "--plan", description: "say how each target would be built, build nothing" },
|
|
3322
|
-
{ flags: "--preset <name>", description: "go, bun, deno, rust, dotnet, electron, pyinstaller or custom" },
|
|
3323
|
+
{ flags: "--preset <name>", description: "go, bun, deno, rust, dotnet, electron, pyinstaller, python or custom" },
|
|
3323
3324
|
{ flags: "--ship", description: "attach what was built to the project's newest save" },
|
|
3324
3325
|
],
|
|
3325
3326
|
examples: [
|
|
@@ -3355,6 +3356,11 @@ SPECS.push(localTwin("push", "submit", local_commands_js_1.localPush, {
|
|
|
3355
3356
|
}, [{ flags: "-f, --force", description: "discard unsaved changes that are in the way" }]));
|
|
3356
3357
|
const REGISTRY = (0, registry_js_1.buildRegistry)(SPECS);
|
|
3357
3358
|
async function main(argv) {
|
|
3359
|
+
const old = (0, node_check_js_1.nodeTooOld)();
|
|
3360
|
+
if (old) {
|
|
3361
|
+
console.error(red(old));
|
|
3362
|
+
return 1;
|
|
3363
|
+
}
|
|
3358
3364
|
const [name, ...rest] = argv;
|
|
3359
3365
|
if (!name || name === "--help" || name === "-h") {
|
|
3360
3366
|
console.log((0, help_js_1.renderHelp)(REGISTRY, version_js_1.VERSION));
|
|
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
14
14
|
*/
|
|
15
15
|
const node_process_1 = __importDefault(require("node:process"));
|
|
16
16
|
const git_remote_js_1 = require("./git_remote.js");
|
|
17
|
+
const node_check_js_1 = require("./node_check.js");
|
|
17
18
|
/**
|
|
18
19
|
* Finish, without cutting the pipe to git mid-sentence.
|
|
19
20
|
*
|
|
@@ -36,7 +37,8 @@ function finish(code) {
|
|
|
36
37
|
node_process_1.default.stdin.pause();
|
|
37
38
|
setTimeout(() => node_process_1.default.exit(code), 5000).unref();
|
|
38
39
|
}
|
|
39
|
-
(0,
|
|
40
|
+
const old = (0, node_check_js_1.nodeTooOld)();
|
|
41
|
+
(old ? Promise.reject(new Error(old)) : (0, git_remote_js_1.main)(node_process_1.default.argv.slice(2)))
|
|
40
42
|
.then(finish)
|
|
41
43
|
.catch((error) => {
|
|
42
44
|
/*
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MINIMUM_NODE = void 0;
|
|
7
|
+
exports.nodeTooOld = nodeTooOld;
|
|
8
|
+
/*
|
|
9
|
+
The Node this needs, said before anything else runs.
|
|
10
|
+
|
|
11
|
+
The package said Node 20.11, and that was wrong: bundles and local histories
|
|
12
|
+
use zstd, which Node gained in 23.8 and 22.15. On Linux, Node 22.12 got as
|
|
13
|
+
far as reading a project and then died with
|
|
14
|
+
"(0 , node_zlib_1.zstdDecompressSync) is not a function", which names
|
|
15
|
+
nothing anybody can act on.
|
|
16
|
+
*/
|
|
17
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
18
|
+
const node_zlib_1 = __importDefault(require("node:zlib"));
|
|
19
|
+
exports.MINIMUM_NODE = "22.15";
|
|
20
|
+
/** A sentence for a Node that is too old, or null when this one will do. */
|
|
21
|
+
function nodeTooOld() {
|
|
22
|
+
if (typeof node_zlib_1.default.zstdDecompressSync === "function")
|
|
23
|
+
return null;
|
|
24
|
+
return (`CodeRook needs Node ${exports.MINIMUM_NODE} or later; this is Node ${node_process_1.default.versions.node} (${node_process_1.default.execPath}).\n` +
|
|
25
|
+
`Install a current Node (https://nodejs.org), or put a newer one first on your PATH.`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.UV_PLATFORM = exports.PYTHON_TRIPLE = void 0;
|
|
7
|
+
exports.tarEntry = tarEntry;
|
|
8
|
+
exports.renamedEntries = renamedEntries;
|
|
9
|
+
exports.scriptOf = scriptOf;
|
|
10
|
+
exports.runtimeRequirements = runtimeRequirements;
|
|
11
|
+
exports.buildPortablePython = buildPortablePython;
|
|
12
|
+
/**
|
|
13
|
+
* A Python program built for another system, without that system.
|
|
14
|
+
*
|
|
15
|
+
* PyInstaller freezes a program with the interpreter it runs under, so it
|
|
16
|
+
* only ever builds for the machine it is on — which left Python as the one
|
|
17
|
+
* language `cbx build` could not take to Linux from Windows. This assembles
|
|
18
|
+
* the same result from parts instead:
|
|
19
|
+
*
|
|
20
|
+
* - the target system's own CPython, from python-build-standalone (the
|
|
21
|
+
* relocatable builds uv and others use), checked against its published
|
|
22
|
+
* SHA-256;
|
|
23
|
+
* - the program's dependencies as the target system's wheels, resolved by
|
|
24
|
+
* uv for that platform — environment markers included, which pip with
|
|
25
|
+
* `--platform` gets wrong because it evaluates them for this machine;
|
|
26
|
+
* - the program's own files, and a launcher.
|
|
27
|
+
*
|
|
28
|
+
* Out comes `<name>-<target>.tar.gz`: one folder holding `python/`, `lib/`,
|
|
29
|
+
* `app/` and the launcher, with every program in it marked executable. The
|
|
30
|
+
* interpreter's archive is never unpacked here — its symlinks would not
|
|
31
|
+
* survive Windows — its entries are carried across with their paths renamed.
|
|
32
|
+
*
|
|
33
|
+
* The limit is a package that publishes no wheel for the target: there is no
|
|
34
|
+
* compiler for another system here, and uv says so by name.
|
|
35
|
+
*/
|
|
36
|
+
const node_child_process_1 = require("node:child_process");
|
|
37
|
+
const node_crypto_1 = require("node:crypto");
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const promises_1 = require("node:fs/promises");
|
|
40
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
41
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
42
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
43
|
+
const node_zlib_1 = require("node:zlib");
|
|
44
|
+
const rules_js_1 = require("../../cbx/src/core/rules.js");
|
|
45
|
+
const worktree_js_1 = require("../../cbx/src/core/worktree.js");
|
|
46
|
+
/** python-build-standalone's name for each system. */
|
|
47
|
+
exports.PYTHON_TRIPLE = {
|
|
48
|
+
"windows-x64": "x86_64-pc-windows-msvc",
|
|
49
|
+
"windows-arm64": "aarch64-pc-windows-msvc",
|
|
50
|
+
"linux-x64": "x86_64-unknown-linux-gnu",
|
|
51
|
+
"linux-arm64": "aarch64-unknown-linux-gnu",
|
|
52
|
+
"macos-x64": "x86_64-apple-darwin",
|
|
53
|
+
"macos-arm64": "aarch64-apple-darwin",
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* uv's name for the platform wheels are chosen for. Linux asks for glibc 2.28
|
|
57
|
+
* (Debian 10, Ubuntu 20.04, RHEL 8 and later), which is where most projects'
|
|
58
|
+
* wheels are built now; older wheels still qualify.
|
|
59
|
+
*/
|
|
60
|
+
exports.UV_PLATFORM = {
|
|
61
|
+
"windows-x64": "x86_64-pc-windows-msvc",
|
|
62
|
+
"windows-arm64": "aarch64-pc-windows-msvc",
|
|
63
|
+
"linux-x64": "x86_64-manylinux_2_28",
|
|
64
|
+
"linux-arm64": "aarch64-manylinux_2_28",
|
|
65
|
+
"macos-x64": "x86_64-apple-darwin",
|
|
66
|
+
"macos-arm64": "aarch64-apple-darwin",
|
|
67
|
+
};
|
|
68
|
+
/* ------------------------------------------------------------------ tar */
|
|
69
|
+
function octal(value, width) {
|
|
70
|
+
return `${value.toString(8).padStart(width - 1, "0")}\0`;
|
|
71
|
+
}
|
|
72
|
+
function checksum(header) {
|
|
73
|
+
header.fill(0x20, 148, 156);
|
|
74
|
+
let sum = 0;
|
|
75
|
+
for (const byte of header)
|
|
76
|
+
sum += byte;
|
|
77
|
+
header.write(`${sum.toString(8).padStart(6, "0")}\0 `, 148, 8, "latin1");
|
|
78
|
+
}
|
|
79
|
+
/** Where a path goes in a ustar header: name alone, or prefix and name. */
|
|
80
|
+
function splitName(full) {
|
|
81
|
+
if (Buffer.byteLength(full) <= 100)
|
|
82
|
+
return { prefix: "", name: full };
|
|
83
|
+
for (let at = full.indexOf("/"); at !== -1; at = full.indexOf("/", at + 1)) {
|
|
84
|
+
const prefix = full.slice(0, at);
|
|
85
|
+
const name = full.slice(at + 1);
|
|
86
|
+
if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100 && name)
|
|
87
|
+
return { prefix, name };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
/** A pax header carrying a path too long for ustar. */
|
|
92
|
+
function paxFor(full) {
|
|
93
|
+
let record = ` path=${full}\n`;
|
|
94
|
+
let length = Buffer.byteLength(record);
|
|
95
|
+
/* The record counts its own length digits. */
|
|
96
|
+
while (Buffer.byteLength(`${length}${record}`) !== length)
|
|
97
|
+
length = Buffer.byteLength(`${length}${record}`);
|
|
98
|
+
const body = Buffer.from(`${length}${record}`);
|
|
99
|
+
const header = Buffer.alloc(512);
|
|
100
|
+
header.write("PaxHeader", 0);
|
|
101
|
+
header.write(octal(0o644, 8), 100, "latin1");
|
|
102
|
+
header.write(octal(0, 8), 108, "latin1");
|
|
103
|
+
header.write(octal(0, 8), 116, "latin1");
|
|
104
|
+
header.write(octal(body.length, 12), 124, "latin1");
|
|
105
|
+
header.write(octal(0, 12), 136, "latin1");
|
|
106
|
+
header.write("x", 156, "latin1");
|
|
107
|
+
header.write("ustar\0", 257, "latin1");
|
|
108
|
+
header.write("00", 263, "latin1");
|
|
109
|
+
checksum(header);
|
|
110
|
+
return Buffer.concat([header, body, Buffer.alloc((512 - (body.length % 512)) % 512)]);
|
|
111
|
+
}
|
|
112
|
+
function setName(header, full) {
|
|
113
|
+
header.fill(0, 0, 100);
|
|
114
|
+
header.fill(0, 345, 500);
|
|
115
|
+
const split = splitName(full);
|
|
116
|
+
if (split) {
|
|
117
|
+
header.write(split.name, 0, 100, "utf8");
|
|
118
|
+
header.write(split.prefix, 345, 155, "utf8");
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
header.write(full.slice(-99), 0, 100, "utf8");
|
|
122
|
+
return [paxFor(full)];
|
|
123
|
+
}
|
|
124
|
+
/** One file, or a symlink, as tar bytes. */
|
|
125
|
+
function tarEntry(full, body, mode, link) {
|
|
126
|
+
const header = Buffer.alloc(512);
|
|
127
|
+
const before = setName(header, full);
|
|
128
|
+
header.write(octal(mode, 8), 100, "latin1");
|
|
129
|
+
header.write(octal(0, 8), 108, "latin1");
|
|
130
|
+
header.write(octal(0, 8), 116, "latin1");
|
|
131
|
+
header.write(octal(link ? 0 : body.length, 12), 124, "latin1");
|
|
132
|
+
header.write(octal(Math.floor(Date.now() / 1000), 12), 136, "latin1");
|
|
133
|
+
header.write(link ? "2" : "0", 156, "latin1");
|
|
134
|
+
if (link)
|
|
135
|
+
header.write(link, 157, 100, "utf8");
|
|
136
|
+
header.write("ustar\0", 257, "latin1");
|
|
137
|
+
header.write("00", 263, "latin1");
|
|
138
|
+
checksum(header);
|
|
139
|
+
const data = link ? [] : [body, Buffer.alloc((512 - (body.length % 512)) % 512)];
|
|
140
|
+
return Buffer.concat([...before, header, ...data]);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Every entry of a ustar archive, renamed under `top/`, header by header.
|
|
144
|
+
* Bodies and link targets are untouched, so symlinks and modes survive.
|
|
145
|
+
*/
|
|
146
|
+
function renamedEntries(archive, top) {
|
|
147
|
+
const out = [];
|
|
148
|
+
let at = 0;
|
|
149
|
+
while (at + 512 <= archive.length) {
|
|
150
|
+
const header = Buffer.from(archive.subarray(at, at + 512));
|
|
151
|
+
if (header.every((byte) => byte === 0))
|
|
152
|
+
break;
|
|
153
|
+
const field = (start, length) => header.subarray(start, start + length).toString("utf8").replace(/\0.*$/s, "");
|
|
154
|
+
const size = parseInt(field(124, 12).trim() || "0", 8);
|
|
155
|
+
const type = String.fromCharCode(header[156] ?? 48);
|
|
156
|
+
const span = 512 + Math.ceil(size / 512) * 512;
|
|
157
|
+
if (type === "x" || type === "g" || type === "L" || type === "K") {
|
|
158
|
+
throw new Error("the interpreter archive uses long-name headers this does not rewrite");
|
|
159
|
+
}
|
|
160
|
+
const prefix = field(345, 155);
|
|
161
|
+
const name = prefix ? `${prefix}/${field(0, 100)}` : field(0, 100);
|
|
162
|
+
const before = setName(header, `${top}/${name}`);
|
|
163
|
+
checksum(header);
|
|
164
|
+
out.push(...before, header, Buffer.from(archive.subarray(at + 512, at + span)));
|
|
165
|
+
at += span;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
const isProgram = (body) => (body[0] === 0x7f && body.toString("latin1", 1, 4) === "ELF") ||
|
|
170
|
+
body.toString("latin1", 0, 2) === "#!" ||
|
|
171
|
+
(body.length >= 4 && [0xfeedfacf, 0xcafebabe, 0xbebafeca].includes(body.readUInt32LE(0)));
|
|
172
|
+
/* ------------------------------------------------------------- runtime */
|
|
173
|
+
function run(program, args, options = {}) {
|
|
174
|
+
return new Promise((done) => {
|
|
175
|
+
let out = "";
|
|
176
|
+
const child = (0, node_child_process_1.spawn)(program, args, { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
177
|
+
child.stdout.on("data", (chunk) => {
|
|
178
|
+
out += chunk;
|
|
179
|
+
if (!options.quiet)
|
|
180
|
+
node_process_1.default.stdout.write(chunk);
|
|
181
|
+
});
|
|
182
|
+
child.stderr.on("data", (chunk) => {
|
|
183
|
+
out += chunk;
|
|
184
|
+
if (!options.quiet)
|
|
185
|
+
node_process_1.default.stderr.write(chunk);
|
|
186
|
+
});
|
|
187
|
+
child.on("error", (error) => done({ code: 1, out: error.message }));
|
|
188
|
+
child.on("exit", (code) => done({ code: code ?? 1, out }));
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/** uv on PATH, or one installed into the cache with this machine's pip. */
|
|
192
|
+
async function findUv(cache) {
|
|
193
|
+
if ((await run("uv", ["--version"], { quiet: true })).code === 0)
|
|
194
|
+
return "uv";
|
|
195
|
+
const local = node_path_1.default.join(cache, "uv", "bin", node_process_1.default.platform === "win32" ? "uv.exe" : "uv");
|
|
196
|
+
if ((0, node_fs_1.existsSync)(local))
|
|
197
|
+
return local;
|
|
198
|
+
for (const python of ["python3", "python"]) {
|
|
199
|
+
const installed = await run(python, ["-m", "pip", "install", "--quiet", "--disable-pip-version-check", "--target", node_path_1.default.join(cache, "uv"), "uv"], { quiet: true });
|
|
200
|
+
if (installed.code === 0 && (0, node_fs_1.existsSync)(local))
|
|
201
|
+
return local;
|
|
202
|
+
}
|
|
203
|
+
throw new Error("needs uv (https://docs.astral.sh/uv) or Python with pip to fetch it");
|
|
204
|
+
}
|
|
205
|
+
/** The target's CPython, downloaded once, checked against its published digest. */
|
|
206
|
+
async function pythonRuntime(cache, version, target, say) {
|
|
207
|
+
const folder = node_path_1.default.join(cache, "python");
|
|
208
|
+
await (0, promises_1.mkdir)(folder, { recursive: true });
|
|
209
|
+
const wanted = new RegExp(`^cpython-${version.replace(".", "\\.")}\\.\\d+\\+\\d+-${exports.PYTHON_TRIPLE[target]}-install_only\\.tar\\.gz$`);
|
|
210
|
+
const cached = (await (0, promises_1.readdir)(folder)).find((name) => wanted.test(name));
|
|
211
|
+
if (cached)
|
|
212
|
+
return (0, promises_1.readFile)(node_path_1.default.join(folder, cached));
|
|
213
|
+
const release = (await (await fetch("https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest", {
|
|
214
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "coderook-cli" },
|
|
215
|
+
})).json());
|
|
216
|
+
const asset = (release.assets ?? []).find((one) => wanted.test(one.name));
|
|
217
|
+
if (!asset)
|
|
218
|
+
throw new Error(`no CPython ${version} build for ${target} in the latest python-build-standalone release`);
|
|
219
|
+
if (!asset.digest?.startsWith("sha256:"))
|
|
220
|
+
throw new Error("the release does not publish a digest for it");
|
|
221
|
+
say(`Fetching ${asset.name}…`);
|
|
222
|
+
const body = Buffer.from(await (await fetch(asset.browser_download_url)).arrayBuffer());
|
|
223
|
+
if ((0, node_crypto_1.createHash)("sha256").update(body).digest("hex") !== asset.digest.slice("sha256:".length)) {
|
|
224
|
+
throw new Error("the download does not match its published digest");
|
|
225
|
+
}
|
|
226
|
+
await (0, promises_1.writeFile)(node_path_1.default.join(folder, asset.name), body);
|
|
227
|
+
return body;
|
|
228
|
+
}
|
|
229
|
+
/* ------------------------------------------------------------- project */
|
|
230
|
+
/** The script PyInstaller would freeze: from a .spec's Analysis, or the file itself. */
|
|
231
|
+
function scriptOf(entry, specText = "") {
|
|
232
|
+
if (!entry.endsWith(".spec"))
|
|
233
|
+
return entry;
|
|
234
|
+
const found = specText.match(/Analysis\(\s*\[\s*(?:r)?['"]([^'"]+)['"]/);
|
|
235
|
+
if (!found)
|
|
236
|
+
throw new Error(`could not find the script in ${entry}; set "entry" to it`);
|
|
237
|
+
/* A spec written on Windows escapes its backslashes: `src\\app\\main.py`. */
|
|
238
|
+
return found[1].replace(/\\+/g, "/");
|
|
239
|
+
}
|
|
240
|
+
/** Requirements, minus the build tools that freeze rather than run the program. */
|
|
241
|
+
function runtimeRequirements(text) {
|
|
242
|
+
return text
|
|
243
|
+
.split(/\r?\n/)
|
|
244
|
+
.filter((line) => !/^\s*(pyinstaller|pyinstaller-hooks-contrib|nuitka|cx[-_]freeze)\b/i.test(line))
|
|
245
|
+
.join("\n");
|
|
246
|
+
}
|
|
247
|
+
const SKIP = new Set([...rules_js_1.UNCOUNTED_DIRECTORIES, "__pycache__", "build", "dist", "venv", ".venv", "env", "node_modules", ".mypy_cache", ".pytest_cache", ".tox"]);
|
|
248
|
+
/** The program's own files, as the project's ignore rules see them. */
|
|
249
|
+
async function appFiles(folder) {
|
|
250
|
+
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
251
|
+
const layers = [{ base: "", rules: (0, rules_js_1.parseRules)(`${rules.shared}\n${rules.local}`) }];
|
|
252
|
+
const found = [];
|
|
253
|
+
const walk = async (relative) => {
|
|
254
|
+
for (const entry of await (0, promises_1.readdir)(node_path_1.default.join(folder, relative), { withFileTypes: true })) {
|
|
255
|
+
const rel = relative ? `${relative}/${entry.name}` : entry.name;
|
|
256
|
+
if (entry.isDirectory()) {
|
|
257
|
+
if (SKIP.has(entry.name) || (0, rules_js_1.excludes)(rel, true, layers))
|
|
258
|
+
continue;
|
|
259
|
+
await walk(rel);
|
|
260
|
+
}
|
|
261
|
+
else if (entry.isFile() && !(0, rules_js_1.excludes)(rel, false, layers) && !/\.(pyc|pyo|spec)$/.test(entry.name)) {
|
|
262
|
+
found.push(rel);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
await walk("");
|
|
267
|
+
return found;
|
|
268
|
+
}
|
|
269
|
+
async function buildPortablePython(request) {
|
|
270
|
+
const say = request.say ?? ((text) => console.log(text));
|
|
271
|
+
const { folder, target } = request;
|
|
272
|
+
const windows = target.startsWith("windows");
|
|
273
|
+
const top = `${request.name}-${target}`;
|
|
274
|
+
const hostVersion = (await run("python3", ["-c", "import sys;print('%d.%d'%sys.version_info[:2])"], { quiet: true }));
|
|
275
|
+
const hostVersionAlt = hostVersion.code === 0 ? hostVersion : await run("python", ["-c", "import sys;print('%d.%d'%sys.version_info[:2])"], { quiet: true });
|
|
276
|
+
const version = request.python ?? (hostVersionAlt.code === 0 ? hostVersionAlt.out.trim() : "3.12");
|
|
277
|
+
const script = scriptOf(request.entry, request.entry.endsWith(".spec") ? await (0, promises_1.readFile)(node_path_1.default.join(folder, request.entry), "utf8") : "");
|
|
278
|
+
if (!(0, node_fs_1.existsSync)(node_path_1.default.join(folder, script)))
|
|
279
|
+
throw new Error(`${script} is not in the project`);
|
|
280
|
+
const runtime = (0, node_zlib_1.gunzipSync)(await pythonRuntime(request.cache, version, target, say));
|
|
281
|
+
const uv = await findUv(request.cache);
|
|
282
|
+
/* Dependencies, as the target's wheels. */
|
|
283
|
+
const scratch = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "coderook-pybuild-"));
|
|
284
|
+
try {
|
|
285
|
+
const lib = node_path_1.default.join(scratch, "lib");
|
|
286
|
+
await (0, promises_1.mkdir)(lib);
|
|
287
|
+
let requirements = "";
|
|
288
|
+
if ((0, node_fs_1.existsSync)(node_path_1.default.join(folder, "requirements.txt"))) {
|
|
289
|
+
requirements = runtimeRequirements(await (0, promises_1.readFile)(node_path_1.default.join(folder, "requirements.txt"), "utf8"));
|
|
290
|
+
}
|
|
291
|
+
if (requirements.trim()) {
|
|
292
|
+
const file = node_path_1.default.join(scratch, "requirements.txt");
|
|
293
|
+
await (0, promises_1.writeFile)(file, requirements);
|
|
294
|
+
say(`Resolving dependencies for ${target} (${exports.UV_PLATFORM[target]}, Python ${version})…`);
|
|
295
|
+
const installed = await run(uv, [
|
|
296
|
+
"pip", "install", "--quiet", "--target", lib,
|
|
297
|
+
"--python-platform", exports.UV_PLATFORM[target], "--python-version", version,
|
|
298
|
+
/* No building from source: it would build for this machine, not the target. */
|
|
299
|
+
"--no-build", "-r", file,
|
|
300
|
+
]);
|
|
301
|
+
if (installed.code !== 0) {
|
|
302
|
+
throw new Error(/no wheels|--no-build|only-binary|has no wheel/i.test(installed.out)
|
|
303
|
+
? `a dependency publishes no ${target} wheel, and it cannot be compiled for ${target} here; build on that system instead`
|
|
304
|
+
: "resolving the dependencies failed (above)");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const entries = renamedEntries(runtime, top);
|
|
308
|
+
const add = async (base, prefix, skipTop = new Set()) => {
|
|
309
|
+
const walk = async (relative) => {
|
|
310
|
+
for (const entry of await (0, promises_1.readdir)(node_path_1.default.join(base, relative), { withFileTypes: true })) {
|
|
311
|
+
const rel = relative ? `${relative}/${entry.name}` : entry.name;
|
|
312
|
+
if (!relative && skipTop.has(entry.name))
|
|
313
|
+
continue;
|
|
314
|
+
if (entry.isDirectory()) {
|
|
315
|
+
if (entry.name === "__pycache__")
|
|
316
|
+
continue;
|
|
317
|
+
await walk(rel);
|
|
318
|
+
}
|
|
319
|
+
else if (entry.isFile()) {
|
|
320
|
+
const body = await (0, promises_1.readFile)(node_path_1.default.join(base, rel));
|
|
321
|
+
entries.push(tarEntry(`${top}/${prefix}/${rel}`, body, isProgram(body) ? 0o755 : 0o644));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
await walk("");
|
|
326
|
+
};
|
|
327
|
+
/* uv writes console scripts for this machine into bin/; they are not the target's. */
|
|
328
|
+
await add(lib, "lib", new Set(["bin"]));
|
|
329
|
+
for (const rel of await appFiles(folder)) {
|
|
330
|
+
const body = await (0, promises_1.readFile)(node_path_1.default.join(folder, rel));
|
|
331
|
+
entries.push(tarEntry(`${top}/app/${rel}`, body, isProgram(body) ? 0o755 : 0o644));
|
|
332
|
+
}
|
|
333
|
+
if (windows) {
|
|
334
|
+
const python = request.gui ? "pythonw.exe" : "python.exe";
|
|
335
|
+
const launcher = [
|
|
336
|
+
"@echo off",
|
|
337
|
+
"setlocal",
|
|
338
|
+
'set "HERE=%~dp0"',
|
|
339
|
+
'set "PYTHONPATH=%HERE%lib;%HERE%app"',
|
|
340
|
+
"set PYTHONNOUSERSITE=1",
|
|
341
|
+
`${request.gui ? 'start "" ' : ""}"%HERE%python\\${python}" "%HERE%app\\${script.replace(/\//g, "\\")}" %*`,
|
|
342
|
+
"",
|
|
343
|
+
].join("\r\n");
|
|
344
|
+
entries.push(tarEntry(`${top}/${request.name}.cmd`, Buffer.from(launcher), 0o755));
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const launcher = [
|
|
348
|
+
"#!/bin/sh",
|
|
349
|
+
"# Runs the program with the Python beside it. Made by cbx build.",
|
|
350
|
+
'here="$(cd "$(dirname "$0")" && pwd -P)"',
|
|
351
|
+
/* Exported, not prefixed: assignments before `exec` are not passed on by every sh. */
|
|
352
|
+
'PYTHONPATH="$here/lib:$here/app${PYTHONPATH:+:$PYTHONPATH}"',
|
|
353
|
+
"PYTHONNOUSERSITE=1",
|
|
354
|
+
"export PYTHONPATH PYTHONNOUSERSITE",
|
|
355
|
+
`exec "$here/python/bin/python3" "$here/app/${script}" "$@"`,
|
|
356
|
+
"",
|
|
357
|
+
].join("\n");
|
|
358
|
+
entries.push(tarEntry(`${top}/${request.name}`, Buffer.from(launcher), 0o755));
|
|
359
|
+
}
|
|
360
|
+
entries.push(Buffer.alloc(1024));
|
|
361
|
+
await (0, promises_1.mkdir)(request.outFolder, { recursive: true });
|
|
362
|
+
const archive = node_path_1.default.join(request.outFolder, `${top}.tar.gz`);
|
|
363
|
+
await (0, promises_1.writeFile)(archive, (0, node_zlib_1.gzipSync)(Buffer.concat(entries), { level: 9 }));
|
|
364
|
+
say(`Wrote ${node_path_1.default.basename(archive)} (${((await (0, promises_1.stat)(archive)).size / 1024 ** 2).toFixed(1)} MB): unpack it and run ${windows ? `${request.name}.cmd` : `./${request.name}`}.`);
|
|
365
|
+
return archive;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
await (0, promises_1.rm)(scratch, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderook/cli",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.1",
|
|
4
4
|
"description": "The command line for CodeRook (coderook.com): version control and project hosting for projects of any size. Every save is a compressed snapshot, private or public, never used to train AI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://coderook.com",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"gamedev"
|
|
21
21
|
],
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=22.15.0"
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
26
26
|
"cbx": "dist/cli/src/cli.js",
|