diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-22 11:46:32 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-22 11:46:32 +0000 |
| commit | 4494b54d0e4b0b1564a7c66391216b98c8523b0e (patch) | |
| tree | 6ed15f551e826b8623581292c4cdab2f05fbf9b9 /bundle | |
| parent | 8d82bcba8f24a1ee30d4dee8a2ba19d2b58e7f4f (diff) | |
build(bundle): port the package smoke test to Perl
Replace bundle/scripts/smoke-package.py with smoke-package.pl, which uses
only core modules like the other build and test scripts. Windows runs it
with native Strawberry Perl.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'bundle')
| -rw-r--r-- | bundle/AGENTS/DEVELOPMENT.md | 2 | ||||
| -rw-r--r-- | bundle/scripts/smoke-package.pl | 106 | ||||
| -rw-r--r-- | bundle/scripts/smoke-package.py | 50 |
3 files changed, 107 insertions, 51 deletions
diff --git a/bundle/AGENTS/DEVELOPMENT.md b/bundle/AGENTS/DEVELOPMENT.md index 1e2678c..456c376 100644 --- a/bundle/AGENTS/DEVELOPMENT.md +++ b/bundle/AGENTS/DEVELOPMENT.md @@ -16,7 +16,7 @@ For a Linux compilation check use `npm run tauri -- build --no-bundle`. Release `npm run package` builds the native application and writes `artifacts/cpsc-bundle-YYYYMMDD-HHMMSS-commit-SYSTEM-ARCH.ext`. Timestamp and short SHA come from the last commit touching the compiler paths in `cmake/CompilerPaths.txt` plus `bundle/` and `plugin/`, using UTC, so unrelated commits do not change the version. `package.js` passes them to the build as `CPSC_BUNDLE_TIME`/`CPSC_BUNDLE_COMMIT`; development builds have none, so the updater is off. Windows publishes only the .exe; no installer or adjacent compiler is required at download time. The application extracts the embedded compiler into the current working directory on startup, retains it there, and reports write failures through the UI. macOS builds the .app, retaining its compiler, then archives it with tar and xz. No Linux package is published. -Licenses and notices are included as frontend assets, including inside the single Windows executable, and as app resources on macOS/Linux. The Windows Tauri config disables externalBin and external resources. Windows uses the system WebView2 runtime. `scripts/smoke-package.py` checks a portable Windows executable (including startup extraction), an extracted .app, or Linux's unbundled release directory and executes its compiler without toolchain directories on PATH. +Licenses and notices are included as frontend assets, including inside the single Windows executable, and as app resources on macOS/Linux. The Windows Tauri config disables externalBin and external resources. Windows uses the system WebView2 runtime. `scripts/smoke-package.pl` checks a portable Windows executable (including startup extraction), an extracted .app, or Linux's unbundled release directory and executes its compiler without toolchain directories on PATH. Nightly extends the existing native compiler jobs: reuse each job's EMBEDDED compiler, compile and test the desktop app on Linux, and upload only the Windows/macOS packages. macOS checks the extracted .tar.xz; Windows checks the single .exe in a fresh working directory. Publishing requires the original four packages plus both desktop packages and retains the existing current-SHA guard and asset replacement behavior. Validate workflow changes with actionlint and a fake gh API, never the real nightly release. diff --git a/bundle/scripts/smoke-package.pl b/bundle/scripts/smoke-package.pl new file mode 100644 index 0000000..919e925 --- /dev/null +++ b/bundle/scripts/smoke-package.pl @@ -0,0 +1,106 @@ +#!/usr/bin/env perl +# Check a portable desktop package without relying on an installed toolchain. +use strict; +use warnings; +use Cwd qw(abs_path); +use File::Basename qw(dirname); +use File::Find (); +use File::Spec; +use File::Temp qw(tempdir); +use IPC::Open3 qw(open3); +use POSIX qw(WNOHANG); +use Time::HiRes qw(sleep time); + +my $IS_WINDOWS = $^O eq 'MSWin32'; + +sub write_file { + my ($path, $layer, $data) = @_; + open my $fh, ">$layer", $path or die "cannot write $path: $!\n"; + print {$fh} $data; + close $fh or die "cannot write $path: $!\n"; +} + +sub read_file { + my ($path) = @_; + open my $fh, '<:raw', $path or die "cannot read $path: $!\n"; + my $data = do { local $/; <$fh> }; + close $fh; + return defined($data) ? $data : ''; +} + +# Reap a child within $seconds, killing it on timeout. Returns its exit code. +sub finish { + my ($pid, $seconds, $what) = @_; + my $deadline = time() + $seconds; + while (waitpid($pid, WNOHANG) == 0) { + if (time() >= $deadline) { + kill 'KILL', $pid; + waitpid($pid, 0); + die "$what timed out\n"; + } + sleep 0.1; + } + return $? >> 8; +} + +my $root = abs_path($ARGV[0]); +die "Missing package path\n" unless defined $root; +# Children inherit %ENV; Win32 %ENV lookups ignore case. +$ENV{PATH} = $IS_WINDOWS + ? join(';', File::Spec->catdir($ENV{SystemRoot}, 'System32'), $ENV{SystemRoot}) + : '/nonexistent'; +# Children also inherit the cwd. Exit cleanup leaves it before removing it. +my $work = tempdir('cpsc bundle smoke XXXXXXXX', TMPDIR => 1, CLEANUP => 1); +chdir $work or die "cannot chdir to $work: $!\n"; +my $compiler; +if ($IS_WINDOWS) { + die "Expected a single portable .exe\n" unless -f $root && $root =~ /\.exe\z/; + # Extraction happens before WebView startup and publishes a complete file by rename. + my $desktop = system 1, $root; + die "cannot start $root: $!\n" unless $desktop > 0; + $compiler = File::Spec->catfile($work, 'cpsc.exe'); + my $extracted = eval { + my $deadline = time() + 30; + until (-f $compiler) { + die "Desktop exited before extracting its compiler\n" if waitpid($desktop, WNOHANG) != 0; + die "Compiler extraction timed out\n" if time() >= $deadline; + sleep 0.1; + } + 1; + }; + my $error = $@; + kill 'KILL', $desktop if waitpid($desktop, WNOHANG) == 0; + finish($desktop, 10, 'Desktop shutdown'); + die $error unless $extracted; +} else { + my @compilers; + File::Find::find(sub { push @compilers, $File::Find::name if $_ eq 'cpsc' }, $root); + die "Expected one packaged compiler, found [@compilers]\n" unless @compilers == 1; + $compiler = $compilers[0]; + my $directory = dirname($compiler); + die "Missing desktop executable\n" unless -f File::Spec->catfile($directory, 'cpsc-bundle'); + my $resources = $^O eq 'darwin' ? File::Spec->catdir(dirname($directory), 'Resources') : $directory; + for my $filename ('LICENSE', 'ThirdPartyNotices.md', 'licenses/vscode.txt', 'licenses/plugin.md') { + die "Missing $filename\n" unless -f File::Spec->catfile($resources, $filename); + } +} +# Text mode: Windows sources and data files use CRLF. +write_file('data.txt', '', "relative file\n"); +my $source = File::Spec->catfile($work, 'source file.cps'); +write_file($source, '', qq{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}); +# Files instead of pipes: Windows cannot poll pipes, and files cannot fill up. +write_file('stdin', ':raw', "17\n"); +open my $in, '<:raw', 'stdin' or die "cannot read stdin: $!\n"; +open my $out, '>:raw', 'stdout' or die "cannot write stdout: $!\n"; +open my $err, '>:raw', 'stderr' or die "cannot write stderr: $!\n"; +my $pid = open3('<&' . fileno($in), '>&' . fileno($out), '>&' . fileno($err), $compiler, $source, '-r'); +my $status = finish($pid, 30, 'Compiler run'); +close $out; +close $err; +my $stdout = read_file('stdout'); +my $stderr = read_file('stderr'); +die "Compiler exited with $status: '$stderr'\n" unless $status == 0; +(my $normalized = $stdout) =~ s/\r\n/\n/g; +die "Unexpected output: '$stdout'\n" unless $normalized eq "17\nrelative file\n"; +die "Unexpected diagnostics: '$stderr'\n" unless $stderr eq ''; +print "Desktop package smoke passed: portable application, compiler, stdin and relative files.\n"; diff --git a/bundle/scripts/smoke-package.py b/bundle/scripts/smoke-package.py deleted file mode 100644 index 44f885f..0000000 --- a/bundle/scripts/smoke-package.py +++ /dev/null @@ -1,50 +0,0 @@ -"""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() -if sys.platform == "win32": - # Windows environ lookups ignore case; the copied dict has uppercase keys. - system_root = os.environ["SystemRoot"] - environment["PATH"] = os.pathsep.join((os.path.join(system_root, "System32"), system_root)) -else: - environment["PATH"] = "/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.") |
