aboutsummaryrefslogtreecommitdiff
path: root/bundle/scripts
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-22 11:46:32 +0000
committer_ov4 <ov4@ov4.org>2026-10-22 11:46:32 +0000
commit4494b54d0e4b0b1564a7c66391216b98c8523b0e (patch)
tree6ed15f551e826b8623581292c4cdab2f05fbf9b9 /bundle/scripts
parent8d82bcba8f24a1ee30d4dee8a2ba19d2b58e7f4f (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/scripts')
-rw-r--r--bundle/scripts/smoke-package.pl106
-rw-r--r--bundle/scripts/smoke-package.py50
2 files changed, 106 insertions, 50 deletions
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.")