aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/nightly-build.yml90
-rw-r--r--AGENTS/ARCH.md13
-rw-r--r--README.md2
-rw-r--r--bundle/.gitignore3
-rw-r--r--bundle/AGENTS/DEVELOPMENT.md12
-rw-r--r--bundle/README.md2
-rw-r--r--bundle/package.json1
-rw-r--r--bundle/scripts/package.js40
-rw-r--r--bundle/scripts/prepare-compiler.js11
-rw-r--r--bundle/scripts/smoke-package.py45
-rw-r--r--bundle/src-tauri/Cargo.lock1
-rw-r--r--bundle/src-tauri/Cargo.toml3
-rw-r--r--bundle/src-tauri/build.rs2
-rw-r--r--bundle/src-tauri/src/compiler.rs41
-rw-r--r--bundle/src-tauri/src/main.rs9
-rw-r--r--bundle/src-tauri/tauri.conf.json14
-rw-r--r--bundle/src-tauri/tauri.windows.conf.json8
-rw-r--r--bundle/vite.config.js12
18 files changed, 290 insertions, 19 deletions
diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml
index 147b491..9264d3f 100644
--- a/.github/workflows/nightly-build.yml
+++ b/.github/workflows/nightly-build.yml
@@ -42,7 +42,9 @@ jobs:
sudo apt-get install -y --no-install-recommends \
build-essential cmake clang-23 llvm-23-dev lld-23 \
libclang-23-dev liblld-23-dev libpolly-23-dev perl \
- zlib1g-dev libzstd-dev libncurses-dev libedit-dev pandoc
+ zlib1g-dev libzstd-dev libncurses-dev libedit-dev pandoc \
+ libwebkit2gtk-4.1-dev libayatana-appindicator3-dev \
+ libxdo-dev librsvg2-dev patchelf
- name: Build the embedded release package
run: |
@@ -102,6 +104,21 @@ jobs:
done < <(readelf -d "$binary" | sed -n 's/.*Shared library: \[\(.*\)\].*/\1/p')
done
+ - uses: actions/setup-node@v7.1.0
+ with:
+ node-version: '22'
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - name: Compile and check the Linux desktop app without publishing it
+ working-directory: bundle
+ run: |
+ npm ci --ignore-scripts
+ npm run prepare:compiler
+ npm run tauri -- build --no-bundle
+ cargo test --release --locked --manifest-path src-tauri/Cargo.toml
+ python3 scripts/smoke-package.py src-tauri/target/release
+
- name: Upload the compiler package
uses: actions/upload-artifact@v7.0.2
with:
@@ -209,6 +226,39 @@ jobs:
esac
done < <(otool -L "$RUNNER_TEMP/nightly-smoke.out" | tail -n +2 | awk '{print $1}')
+ - uses: actions/setup-node@v7.1.0
+ with:
+ node-version: '22'
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - name: Build and test the macOS desktop package
+ working-directory: bundle
+ run: |
+ npm ci --ignore-scripts
+ npm run prepare:compiler
+ npm run package
+ cargo test --release --locked --manifest-path src-tauri/Cargo.toml
+
+ - name: Smoke test the macOS desktop archive
+ run: |
+ packages=(bundle/artifacts/cpsc-bundle-*-Darwin-arm64.tar.xz)
+ test "${#packages[@]}" -eq 1
+ smoke_dir="$RUNNER_TEMP/cpsc-bundle-smoke"
+ mkdir -p "$smoke_dir"
+ tar -xJf "${packages[0]}" -C "$smoke_dir"
+ python3 bundle/scripts/smoke-package.py "$smoke_dir/CPSC Bundle.app"
+ test "$(lipo -archs "$smoke_dir/CPSC Bundle.app/Contents/MacOS/cpsc-bundle")" = arm64
+ test "$(lipo -archs "$smoke_dir/CPSC Bundle.app/Contents/MacOS/cpsc")" = arm64
+
+ - name: Upload the macOS desktop package
+ uses: actions/upload-artifact@v7.0.2
+ with:
+ name: nightly-bundle-macos
+ path: bundle/artifacts/cpsc-bundle-*-Darwin-arm64.tar.xz
+ if-no-files-found: error
+ retention-days: 1
+
- name: Upload the macOS package
uses: actions/upload-artifact@v7.0.2
with:
@@ -347,6 +397,36 @@ jobs:
$env:PATH = $savedPath
}
+ - uses: actions/setup-node@v7.1.0
+ with:
+ node-version: '22'
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - name: Build and test the Windows desktop package
+ working-directory: bundle
+ run: |
+ npm ci --ignore-scripts
+ npm run prepare:compiler
+ npm run package
+ cargo test --release --locked --manifest-path src-tauri/Cargo.toml
+
+ - name: Smoke test the portable Windows application
+ shell: pwsh
+ run: |
+ $packages = @(Get-ChildItem -Path bundle/artifacts/cpsc-bundle-*-Windows-x86_64.exe -File)
+ if ($packages.Count -ne 1) { throw 'Expected one portable Windows executable' }
+ python bundle/scripts/smoke-package.py $packages[0].FullName
+ if ($LASTEXITCODE -ne 0) { throw 'Desktop package smoke test failed' }
+
+ - name: Upload the Windows desktop package
+ uses: actions/upload-artifact@v7.0.2
+ with:
+ name: nightly-bundle-windows
+ path: bundle/artifacts/cpsc-bundle-*-Windows-x86_64.exe
+ if-no-files-found: error
+ retention-days: 1
+
- name: Upload the Windows package
uses: actions/upload-artifact@v7.0.2
with:
@@ -430,13 +510,15 @@ jobs:
macos=(release-packages/nightly-macos/cpsc-*-Darwin-arm64.tar.xz)
windows=(release-packages/nightly-windows/cpsc-*-Windows-*.zip)
plugin=(release-packages/nightly-plugin/*.vsix)
- for count in "${#linux[@]}" "${#macos[@]}" "${#windows[@]}" "${#plugin[@]}"; do
+ bundle_macos=(release-packages/nightly-bundle-macos/cpsc-bundle-*-Darwin-arm64.tar.xz)
+ bundle_windows=(release-packages/nightly-bundle-windows/cpsc-bundle-*-Windows-x86_64.exe)
+ for count in "${#linux[@]}" "${#macos[@]}" "${#windows[@]}" "${#plugin[@]}" "${#bundle_macos[@]}" "${#bundle_windows[@]}"; do
if [[ "$count" -ne 1 ]]; then
- echo "Expected exactly one package for each of the four builds; leaving the release untouched." >&2
+ echo "Expected exactly one of each of the six packages; leaving the release untouched." >&2
exit 1
fi
done
- packages=("${linux[@]}" "${macos[@]}" "${windows[@]}" "${plugin[@]}")
+ packages=("${linux[@]}" "${macos[@]}" "${windows[@]}" "${plugin[@]}" "${bundle_macos[@]}" "${bundle_windows[@]}")
for package in "${packages[@]}"; do
test -s "$package"
done
diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md
index 84d94ed..1f1aeba 100644
--- a/AGENTS/ARCH.md
+++ b/AGENTS/ARCH.md
@@ -270,10 +270,21 @@ filters. A shared concurrency group cancels superseded runs. A final publish job
requires all four build jobs to succeed before updating the existing `nightly-build`
release. Any build or check failure leaves all previous attachments untouched.
The publish job deletes all old uploaded assets, including paginated results,
-then uploads the four new packages. It never creates or deletes the release
+then uploads the six new packages. It never creates or deletes the release
and preserves its tag, title and notes. GitHub's generated
source downloads remain available.
+The native compiler jobs also build CPSC Bundle, reusing their EMBEDDED executable.
+Linux compiles and tests the app without publishing an installer. Windows x64
+adds a standalone .exe and macOS arm64 adds a .tar.xz containing the .app,
+both with HEAD-based filenames. Windows embeds its compiler and extracts it into
+the current working directory on startup; macOS keeps the compiler inside the app.
+Native runner tests check prefilled input, EOF, diagnostics, output before exit,
+stopping and restart. Portable packages are checked for the desktop executable,
+compiler and notices, then run with toolchain directories removed from PATH.
+The publish job requires both Bundle packages alongside the original four packages.
+See `bundle/AGENTS/DEVELOPMENT.md` for the shared editor and build details.
+
The Linux compiler and plugin jobs use Ubuntu 24.04 x86_64; the macOS compiler
job uses a macOS 15 arm64 runner, and Windows uses Windows Server 2025 x86_64.
All four check out the push SHA and run independently. All three
diff --git a/README.md b/README.md
index e419a95..d5268bb 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ A compiler for CIE Pseudocode (A Level 9618 Computer Science)
[Compiler tests](tests/README.md)
-Download the Linux x86_64, Windows x86_64 or macOS 15+ Apple Silicon compiler package and VS Code extension from [Nightly Build](https://github.com/ovfor4/cie-pseudocode-compiler/releases/tag/nightly-build). The compiler uses EMBEDDED mode and needs no LLVM installation. Each push to `main` builds all four packages in parallel. Release attachments are updated only after all four builds and their checks succeed; any build or check failure keeps all previous attachments.
+Download the Linux x86_64, Windows x86_64 or macOS 15+ Apple Silicon compiler package, VS Code extension, or [CPSC Bundle desktop editor](bundle/README.md) from [Nightly Build](https://github.com/ovfor4/cie-pseudocode-compiler/releases/tag/nightly-build). Bundle includes the compiler as a standalone Windows x64 `.exe` or a macOS Apple Silicon `.tar.xz` containing the `.app`. The compiler uses EMBEDDED mode and needs no LLVM installation. Each push to `main` builds all six packages and checks Linux desktop compilation. Release attachments are updated only after every build and check succeeds; any build or check failure keeps all previous attachments.
Details can be found here: [Pseudocode Guide for Teachers](https://www.cambridgeinternational.org/Images/697401-2026-pseudocode-guide-for-teachers.pdf)
diff --git a/bundle/.gitignore b/bundle/.gitignore
index e215a8a..1404061 100644
--- a/bundle/.gitignore
+++ b/bundle/.gitignore
@@ -2,5 +2,6 @@ node_modules/
dist/
src-tauri/target/
src-tauri/gen/
-src-tauri/binaries/cpsc-*
+src-tauri/binaries/*
artifacts/
+__pycache__/
diff --git a/bundle/AGENTS/DEVELOPMENT.md b/bundle/AGENTS/DEVELOPMENT.md
index 36e87a2..e51fc1e 100644
--- a/bundle/AGENTS/DEVELOPMENT.md
+++ b/bundle/AGENTS/DEVELOPMENT.md
@@ -10,15 +10,21 @@ npm run prepare:compiler
npm run tauri -- dev
```
-`prepare:compiler` copies `../build/cpsc` (or `cpsc.exe`) with the Rust host target suffix needed by Tauri's externalBin packaging. An optional argument selects another compiler executable. Windows uses MSVC Rust for the shell and retains the existing MinGW compiler as a separate process.
+`prepare:compiler` copies `../build/cpsc` with the Rust host target suffix needed by Tauri's externalBin packaging on macOS/Linux. On Windows it compresses `cpsc.exe` into an embedded gzip payload. An optional argument selects another compiler executable. Windows uses MSVC Rust for the shell and retains the existing MinGW compiler as a separate process.
-For a Linux compilation check use `npm run tauri -- build --no-bundle`. Release targets are NSIS on Windows x64 and DMG on macOS arm64. Keep dependency lockfiles committed.
+For a Linux compilation check use `npm run tauri -- build --no-bundle`. Release packages are a standalone executable on Windows x64 and a .tar.xz containing the .app on macOS arm64. Keep dependency lockfiles committed.
+
+`npm run package` builds the native application and writes `artifacts/cpsc-bundle-YYYYMMDD-HHMMSS-commit-SYSTEM-ARCH.ext`. Timestamp and short SHA come from HEAD, using UTC, as with the existing compiler and plugin. The development version stays unchanged. 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.
+
+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.
The editor imports the plugin's grammar, language configuration and pure CommonJS modules directly. Vite's CommonJS adapter includes those sources. Do not duplicate their rules or add a second tokenizer. Dark+ and its parent theme are pinned under `vendor/vscode/`; retain upstream attribution.
The language editing core (grammar, configuration, indentation, lexical masking and bracket matching) is shared in full. Platform editor API adapters, file dialogs and process management are separate. Keep the plugin free of Bundle runtime dependencies.
-The Rust runner launches the sibling `cpsc` executable directly, using the source directory as cwd. Tokio concurrently writes prefilled stdin and drains both output pipes; stdin is dropped after writing. A watch signal kills and reaps the process on Stop or application exit. The output Channel sends raw byte chunks (one streaming TextDecoder per stream in the frontend), then a finished event after both pipes reach EOF. EMBEDDED execution disables the host CRT stdout buffer before JIT initialization; Windows JIT lookups prefer the same msvcrt runtime. Do not introduce a shell or pseudo-terminal.
+The Rust runner launches the packaged compiler directly, using the source directory as cwd. On Windows its absolute path is captured after extracting into the startup working directory; elsewhere it is the sibling `cpsc` executable. Tokio concurrently writes prefilled stdin and drains both output pipes; stdin is dropped after writing. A watch signal kills and reaps the process on Stop or application exit. The output Channel sends raw byte chunks (one streaming TextDecoder per stream in the frontend), then a finished event after both pipes reach EOF. EMBEDDED execution disables the host CRT stdout buffer before JIT initialization; Windows JIT lookups prefer the same msvcrt runtime. Do not introduce a shell or pseudo-terminal.
After building the compiler, run `cargo test --release --manifest-path src-tauri/Cargo.toml`. The two native tests exercise real compilation, prefilled input, EOF, Unicode, diagnostics, output before process exit, stopping, single-run exclusion and restart, including source paths with spaces. CI runs them on all three native platforms.
diff --git a/bundle/README.md b/bundle/README.md
index 3021f3b..3e08ddc 100644
--- a/bundle/README.md
+++ b/bundle/README.md
@@ -2,6 +2,6 @@
Open a `.cps` file, enter its input in **Input**, and click **Run**. The file is saved before running. Program output and errors appear in **Output**. Click **Stop** to end a running program.
-Nightly packages support Windows x64 and macOS Apple Silicon. The compiler is included.
+Nightly provides a standalone Windows x64 `.exe` and a macOS Apple Silicon `.tar.xz` containing the `.app`. Windows extracts the included `cpsc.exe` into the current working directory on startup; macOS keeps it inside the `.app`. Windows uses the system WebView2 runtime.
For development and build instructions, see [AGENTS/DEVELOPMENT.md](AGENTS/DEVELOPMENT.md).
diff --git a/bundle/package.json b/bundle/package.json
index 133d6c5..a0d68e3 100644
--- a/bundle/package.json
+++ b/bundle/package.json
@@ -11,6 +11,7 @@
"dev": "vite --host 127.0.0.1",
"build": "vite build",
"tauri": "tauri",
+ "package": "node scripts/package.js",
"prepare:compiler": "node scripts/prepare-compiler.js"
},
"dependencies": {
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.")
diff --git a/bundle/src-tauri/Cargo.lock b/bundle/src-tauri/Cargo.lock
index 7fa057e..c461524 100644
--- a/bundle/src-tauri/Cargo.lock
+++ b/bundle/src-tauri/Cargo.lock
@@ -392,6 +392,7 @@ dependencies = [
name = "cpsc-bundle"
version = "0.1.0"
dependencies = [
+ "flate2",
"serde",
"tauri",
"tauri-build",
diff --git a/bundle/src-tauri/Cargo.toml b/bundle/src-tauri/Cargo.toml
index 4a61998..573db28 100644
--- a/bundle/src-tauri/Cargo.toml
+++ b/bundle/src-tauri/Cargo.toml
@@ -13,6 +13,9 @@ tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["process", "io-util", "sync", "macros", "rt-multi-thread", "time"] }
+[target.'cfg(windows)'.dependencies]
+flate2 = "1"
+
[profile.release]
strip = true
lto = true
diff --git a/bundle/src-tauri/build.rs b/bundle/src-tauri/build.rs
index d860e1e..bf7e5df 100644
--- a/bundle/src-tauri/build.rs
+++ b/bundle/src-tauri/build.rs
@@ -1,3 +1,5 @@
fn main() {
+ #[cfg(windows)]
+ println!("cargo:rerun-if-changed=binaries/cpsc.exe.gz");
tauri_build::build()
}
diff --git a/bundle/src-tauri/src/compiler.rs b/bundle/src-tauri/src/compiler.rs
new file mode 100644
index 0000000..275a707
--- /dev/null
+++ b/bundle/src-tauri/src/compiler.rs
@@ -0,0 +1,41 @@
+use std::path::PathBuf;
+
+// Keep startup failures available to the UI, including an unwritable working directory.
+pub struct Compiler(pub Result<PathBuf, String>);
+
+impl Compiler {
+ pub fn load() -> Self {
+ Self(executable().map_err(|error| format!("Cannot load compiler: {error}")))
+ }
+}
+
+#[cfg(not(windows))]
+fn executable() -> std::io::Result<PathBuf> {
+ Ok(std::env::current_exe()?.with_file_name("cpsc"))
+}
+
+#[cfg(windows)]
+fn executable() -> std::io::Result<PathBuf> {
+ use std::{
+ fs::{self, File},
+ io,
+ };
+
+ let directory = std::env::current_dir()?;
+ let destination = directory.join("cpsc.exe");
+ let staged = directory.join(format!(".cpsc-{}.tmp", std::process::id()));
+ let result = (|| {
+ let mut compressed = flate2::read::GzDecoder::new(
+ &include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/binaries/cpsc.exe.gz"))[..],
+ );
+ let mut file = File::create(&staged)?;
+ io::copy(&mut compressed, &mut file)?;
+ drop(file);
+ fs::rename(&staged, &destination)?;
+ Ok(destination)
+ })();
+ if result.is_err() {
+ let _ = fs::remove_file(staged);
+ }
+ result
+}
diff --git a/bundle/src-tauri/src/main.rs b/bundle/src-tauri/src/main.rs
index 96f040c..07f25b3 100644
--- a/bundle/src-tauri/src/main.rs
+++ b/bundle/src-tauri/src/main.rs
@@ -1,6 +1,8 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+mod compiler;
mod runner;
+use compiler::Compiler;
use runner::{OutputEvent, Runner};
use tauri::{ipc::Channel, Manager, State};
@@ -20,10 +22,9 @@ async fn run_file(
input: String,
output: Channel<OutputEvent>,
runner: State<'_, Runner>,
+ compiler: State<'_, Compiler>,
) -> Result<(), String> {
- let executable = std::env::current_exe()
- .map_err(|error| error.to_string())?
- .with_file_name(if cfg!(windows) { "cpsc.exe" } else { "cpsc" });
+ let executable = compiler.inner().0.clone()?;
let result = runner
.run(executable, path.into(), input, |stream, bytes| {
let _ = output.send(OutputEvent::Data {
@@ -47,9 +48,11 @@ async fn stop_run(runner: State<'_, Runner>) -> Result<(), String> {
}
fn main() {
+ let compiler = Compiler::load();
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(Runner::default())
+ .manage(compiler)
.invoke_handler(tauri::generate_handler![
read_file, write_file, run_file, stop_run
])
diff --git a/bundle/src-tauri/tauri.conf.json b/bundle/src-tauri/tauri.conf.json
index 3d2e88a..d343e51 100644
--- a/bundle/src-tauri/tauri.conf.json
+++ b/bundle/src-tauri/tauri.conf.json
@@ -18,8 +18,20 @@
"bundle": {
"active": true,
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
- "targets": ["nsis", "dmg"],
+ "targets": ["app"],
"externalBin": ["binaries/cpsc"],
+ "resources": {
+ "../../LICENSE": "LICENSE",
+ "../ThirdPartyNotices.md": "ThirdPartyNotices.md",
+ "../../plugin/ThirdPartyNotices.md": "licenses/plugin.md",
+ "../vendor/vscode/LICENSE.txt": "licenses/vscode.txt",
+ "../node_modules/monaco-editor/LICENSE": "licenses/monaco-editor.txt",
+ "../node_modules/vscode-textmate/LICENSE.md": "licenses/vscode-textmate.txt",
+ "../node_modules/vscode-oniguruma/LICENSE.txt": "licenses/vscode-oniguruma.txt",
+ "../node_modules/jsonc-parser/LICENSE.md": "licenses/jsonc-parser.txt",
+ "../node_modules/@tauri-apps/api/LICENSE-MIT": "licenses/tauri-mit.txt",
+ "../node_modules/@tauri-apps/api/LICENSE-APACHE-2.0": "licenses/tauri-apache.txt"
+ },
"macOS": { "minimumSystemVersion": "15.0" }
}
}
diff --git a/bundle/src-tauri/tauri.windows.conf.json b/bundle/src-tauri/tauri.windows.conf.json
new file mode 100644
index 0000000..c0b39f5
--- /dev/null
+++ b/bundle/src-tauri/tauri.windows.conf.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "bundle": {
+ "active": false,
+ "externalBin": [],
+ "resources": []
+ }
+}
diff --git a/bundle/vite.config.js b/bundle/vite.config.js
index 9721f16..cf173c1 100644
--- a/bundle/vite.config.js
+++ b/bundle/vite.config.js
@@ -1,8 +1,18 @@
import { defineConfig } from 'vite';
import commonjs from '@rollup/plugin-commonjs';
+import { readFileSync } from 'node:fs';
+import configuration from './src-tauri/tauri.conf.json';
export default defineConfig({
- plugins: [commonjs({ include: /plugin\/src\// })],
+ plugins: [commonjs({ include: /plugin\/src\// }), {
+ name: 'bundled-notices',
+ generateBundle() {
+ // Frontend assets are embedded in the portable Windows executable too.
+ for (const [source, fileName] of Object.entries(configuration.bundle.resources)) {
+ this.emitFile({ type: 'asset', fileName, source: readFileSync(new URL(`./src-tauri/${source}`, import.meta.url)) });
+ }
+ }
+ }],
server: { port: 1420, strictPort: true },
clearScreen: false,
build: { target: 'es2022' }