aboutsummaryrefslogtreecommitdiff
path: root/bundle/scripts
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-21 11:22:38 +0000
committer_ov4 <ov4@ov4.org>2026-10-21 11:22:38 +0000
commitfcb9e2ef516cff0b6d5168bc6465e4615d2aa96e (patch)
treeea490c33061dc0bdc56488f99a355c7626aca348 /bundle/scripts
parent7b94b9003daa1126ce78df37481fc52774c2f0b4 (diff)
ci(bundle): publish portable Windows and macOS desktop packages
Build and check Bundle alongside each native compiler. Publish a single Windows exe with an embedded compiler extracted into the startup directory, and a macOS tar.xz containing the app and compiler. Check Linux without publishing a desktop package and require all six nightly attachments. Include licenses in embedded assets and app resources. Verify local builds, package smoke checks and release replacement with a simulated GitHub API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'bundle/scripts')
-rw-r--r--bundle/scripts/package.js40
-rw-r--r--bundle/scripts/prepare-compiler.js11
-rw-r--r--bundle/scripts/smoke-package.py45
3 files changed, 93 insertions, 3 deletions
diff --git a/bundle/scripts/package.js b/bundle/scripts/package.js
new file mode 100644
index 0000000..56aae85
--- /dev/null
+++ b/bundle/scripts/package.js
@@ -0,0 +1,40 @@
+import { execFileSync } from 'node:child_process';
+import { copyFileSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const root = fileURLToPath(new URL('..', import.meta.url));
+
+try {
+ const platform = {
+ win32: { architecture: 'x64', name: 'Windows-x86_64', extension: '.exe' },
+ darwin: { architecture: 'arm64', name: 'Darwin-arm64', extension: '.tar.xz' }
+ }[process.platform];
+ if (!platform || process.arch !== platform.architecture) {
+ throw new Error('Release packages are built on Windows x64 and macOS arm64 only. Use tauri build --no-bundle for Linux.');
+ }
+ const revision = execFileSync('git', ['log', '-1', '--format=%ct:%h'], { cwd: root, encoding: 'utf8' }).trim();
+ const match = /^(\d+):([0-9a-f]+)$/.exec(revision);
+ if (!match) throw new Error('Cannot read HEAD for the package name.');
+ const timestamp = new Date(Number(match[1]) * 1000).toISOString();
+ const date = timestamp.slice(0, 10).replace(/-/g, '');
+ const time = timestamp.slice(11, 19).replace(/:/g, '');
+ execFileSync(process.execPath, [path.join(root, 'node_modules/@tauri-apps/cli/tauri.js'), 'build',
+ ...(process.platform === 'win32' ? ['--no-bundle'] : ['--bundles', 'app'])], { cwd: root, stdio: 'inherit' });
+ const artifacts = path.join(root, 'artifacts');
+ rmSync(artifacts, { recursive: true, force: true });
+ mkdirSync(artifacts, { recursive: true });
+ const name = `cpsc-bundle-${date}-${time}-${match[2]}-${platform.name}${platform.extension}`;
+ if (process.platform === 'win32') {
+ copyFileSync(path.join(root, 'src-tauri/target/release/cpsc-bundle.exe'), path.join(artifacts, name));
+ } else {
+ const built = path.join(root, 'src-tauri/target/release/bundle/macos');
+ const apps = readdirSync(built).filter(name => name.endsWith('.app'));
+ if (apps.length !== 1) throw new Error(`Expected one .app, found ${apps.length}.`);
+ execFileSync('tar', ['-cJf', path.join(artifacts, name), '-C', built, apps[0]], { stdio: 'inherit' });
+ }
+ console.log(`Packaged: ${name}`);
+} catch (error) {
+ console.error(`Cannot package Bundle: ${error.message}`);
+ process.exitCode = 1;
+}
diff --git a/bundle/scripts/prepare-compiler.js b/bundle/scripts/prepare-compiler.js
index de5346a..8568ae9 100644
--- a/bundle/scripts/prepare-compiler.js
+++ b/bundle/scripts/prepare-compiler.js
@@ -1,5 +1,6 @@
import { execFileSync } from 'node:child_process';
-import { copyFileSync, mkdirSync } from 'node:fs';
+import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { gzipSync } from 'node:zlib';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -7,7 +8,11 @@ const root = fileURLToPath(new URL('..', import.meta.url));
const target = execFileSync('rustc', ['--print', 'host-tuple'], { encoding: 'utf8' }).trim();
const extension = process.platform === 'win32' ? '.exe' : '';
const source = path.resolve(process.argv[2] || path.join(root, '..', 'build', `cpsc${extension}`));
-const destination = path.join(root, 'src-tauri', 'binaries', `cpsc-${target}${extension}`);
+const destination = path.join(root, 'src-tauri', 'binaries', process.platform === 'win32' ? 'cpsc.exe.gz' : `cpsc-${target}${extension}`);
mkdirSync(path.dirname(destination), { recursive: true });
-copyFileSync(source, destination);
+if (process.platform === 'win32') {
+ writeFileSync(destination, gzipSync(readFileSync(source)));
+} else {
+ copyFileSync(source, destination);
+}
console.log(`Bundled compiler: ${source}`);
diff --git a/bundle/scripts/smoke-package.py b/bundle/scripts/smoke-package.py
new file mode 100644
index 0000000..98ee52a
--- /dev/null
+++ b/bundle/scripts/smoke-package.py
@@ -0,0 +1,45 @@
+"""Check a portable desktop package without relying on an installed toolchain."""
+
+from pathlib import Path
+import os
+import subprocess
+import sys
+import tempfile
+import time
+
+root = Path(sys.argv[1]).resolve()
+environment = os.environ.copy()
+environment["PATH"] = os.pathsep.join((os.path.join(environment["SystemRoot"], "System32"), environment["SystemRoot"])) if sys.platform == "win32" else "/nonexistent"
+with tempfile.TemporaryDirectory(prefix="cpsc bundle smoke ") as directory:
+ work = Path(directory)
+ if sys.platform == "win32":
+ assert root.is_file() and root.suffix == ".exe", "Expected a single portable .exe"
+ # Extraction happens before WebView startup and publishes a complete file by rename.
+ desktop = subprocess.Popen([str(root)], cwd=work, env=environment)
+ compiler = work / "cpsc.exe"
+ try:
+ deadline = time.monotonic() + 30
+ while not compiler.is_file():
+ assert desktop.poll() is None, "Desktop exited before extracting its compiler"
+ assert time.monotonic() < deadline, "Compiler extraction timed out"
+ time.sleep(0.1)
+ finally:
+ if desktop.poll() is None:
+ desktop.terminate()
+ desktop.wait(timeout=10)
+ else:
+ compilers = list(root.rglob("cpsc"))
+ assert len(compilers) == 1, f"Expected one packaged compiler, found {compilers}"
+ compiler = compilers[0]
+ assert compiler.with_name("cpsc-bundle").is_file(), "Missing desktop executable"
+ resources = compiler.parent.parent / "Resources" if sys.platform == "darwin" else compiler.parent
+ for filename in ("LICENSE", "ThirdPartyNotices.md", "licenses/vscode.txt", "licenses/plugin.md"):
+ assert (resources / filename).is_file(), f"Missing {filename}"
+ (work / "data.txt").write_text("relative file\n", encoding="utf-8")
+ source = work / "source file.cps"
+ source.write_text('DECLARE N : INTEGER\nINPUT N\nOUTPUT N\nDECLARE S : STRING\nOPENFILE "data.txt" FOR READ\nREADFILE "data.txt", S\nCLOSEFILE "data.txt"\nOUTPUT S\n', encoding="utf-8")
+ result = subprocess.run([str(compiler), str(source), "-r"], input=b"17\n", capture_output=True, cwd=work, env=environment, timeout=30)
+ assert result.returncode == 0, result.stderr
+ assert result.stdout.replace(b"\r\n", b"\n") == b"17\nrelative file\n", result.stdout
+ assert result.stderr == b"", result.stderr
+print("Desktop package smoke passed: portable application, compiler, stdin and relative files.")