Skip to content

Fix terminal launcher packaging with one Tauri executable - #354

Merged
shunichironomura merged 5 commits into
mainfrom
hotfix-terminal-launchers
Jul 22, 2026
Merged

Fix terminal launcher packaging with one Tauri executable#354
shunichironomura merged 5 commits into
mainfrom
hotfix-terminal-launchers

Conversation

@shunichironomura

@shunichironomura shunichironomura commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

This is the v0.0.5 hotfix for the Windows TaskDialogIndirect startup failure introduced by the v0.0.4 terminal command.

The fix separates the two responsibilities that v0.0.4 accidentally combined:

  • procnote-tauri is the only Tauri application. It owns the window, plugins, Windows manifest, resources, subsystem, and GUI runtime configuration.
  • procnote-launcher is a small, Tauri-free console executable. It owns the public procnote [WORKSPACE] command, prints help/version/errors synchronously, and starts the packaged GUI asynchronously.
procnote --help / --version / invalid arguments
        │
        └── console launcher prints and exits; GUI is never started

procnote [WORKSPACE]
        │
        ├── console launcher resolves the packaged GUI
        ├── child inherits CWD and environment
        ├── workspace is passed without shell interpolation
        └── GUI is detached; launcher immediately exits

The release version is bumped to 0.0.5 because v0.0.4 already has published artifacts.

Testing the package on a separate Windows machine also exposed an existing persistence bug: the launcher worked, but starting an execution failed with Access is denied. (os error 5). This PR now fixes that Windows directory-sync failure as well.

Review-history note: the first commit on this branch used shell wrappers. While reviewing the public command contract, we found that those wrappers discarded --help and --version output. The second commit replaces them with the native console launcher described here. A third commit fixes the Windows execution-persistence failure found during native package testing. The final PR diff contains no shell or batch launcher.

Why v0.0.4 fails on Windows

v0.0.4 built two executables from different Cargo packages:

  1. procnote-tauri, built by the normal Tauri packaging flow; and
  2. procnote-cli, built separately with cargo build and copied into cli/procnote.exe.

Although the second package called into the Tauri library, Cargo build-script output is package-scoped. Settings emitted by src-tauri/build.rs for procnote-tauri were therefore not automatically applied to procnote-cli.

That distinction is critical on Windows:

  • tauri-plugin-dialog reaches rfd, which imports TaskDialogIndirect.
  • TaskDialogIndirect requires the Common Controls v6 activation context selected by an application manifest.
  • The normal GUI executable had Tauri's Common Controls v6 manifest.
  • The independently built CLI executable did not, so Windows attempted to start it without the required activation context and reported that TaskDialogIndirect could not be found.

The separate build also bypassed other package-scoped linker and runtime settings. Adding a one-off manifest to the old CLI would fix only the visible symptom and preserve the same design trap.

Native launcher architecture

A real console frontend, not a second Tauri app

crates/procnote-launcher depends on Clap, thiserror, and small platform APIs. It deliberately does not depend on procnote-tauri, Tauri, rfd, or any GUI/dialog library.

This means two native executables are installed, but only one is a Tauri application:

Executable role Tauri linked? Subsystem/behavior
Terminal launcher No Console application; synchronous CLI output
Desktop GUI Yes Normal Tauri GUI application

This directly prevents the original TaskDialogIndirect dependency from entering the launcher.

Public command behavior

Clap runs in the console launcher, so terminal behavior is deterministic:

  • procnote --help prints usage and exits successfully.
  • procnote --version prints procnote 0.0.5 and exits successfully.
  • Invalid options print to stderr and return exit code 2.
  • procnote [WORKSPACE] starts the GUI and returns immediately.
  • A workspace beginning with - remains usable through procnote -- <workspace>.

The launcher passes -- and the workspace as separate OS-native arguments to the GUI. It never constructs a shell command, so spaces and shell metacharacters do not change argument boundaries.

Process behavior

std::process::Command inherits the caller's working directory and environment by default. The launcher intentionally leaves both unchanged, allowing the GUI to canonicalize relative paths such as . in the terminal's original context.

Only GUI launch actions are detached:

  • macOS/Linux: the child calls POSIX setsid, receives null standard streams, and starts in a new session without a controlling terminal.
  • Windows: the child is created with DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP and null standard streams.

Help, version, and parser errors never reach the spawn path.

Package layouts

Windows

procnote.exe                  # Tauri GUI, Windows GUI subsystem
bin/procnote.exe              # Tauri-free console launcher
installer/update-user-path.ps1

The NSIS installer adds bin to the current user's PATH. The MSI contains the same launcher but currently requires manual PATH configuration.

macOS

procnote.app/Contents/MacOS/procnote               # Tauri GUI
procnote.app/Contents/Resources/bin/procnote        # console launcher

The launcher resolves Homebrew/manual symlinks before locating the GUI. The Homebrew cask links the console launcher rather than the GUI executable.

Linux

/usr/bin/procnote-gui         # Tauri GUI
/usr/bin/procnote             # console launcher

Debian and RPM package mappings install the launcher automatically. AppImage setup remains manual.

Reproducible launcher packaging

scripts/prepare-launcher.mjs uses Tauri's target environment to:

  1. build procnote-launcher for the same target triple/profile as the package;
  2. locate Cargo's configured target directory through cargo metadata;
  3. clear the old staging directory;
  4. copy the exact target binary into ignored src-tauri/launchers/bin/; and
  5. preserve executable permissions on Unix.

Generated binary paths cannot live in Tauri's automatically loaded platform configuration: tauri-build validates resources during direct Cargo builds, before a generated launcher exists. To keep clean-checkout cargo check and cargo test working, launcher mappings live in explicit tauri.bundle.*.conf.json extensions.

scripts/tauri.mjs wraps the local Tauri CLI and injects the correct extension for desktop build commands, including cross-architecture macOS builds. The release action explicitly uses this wrapper through tauriScript: "pnpm run tauri". User-provided Tauri config extensions remain later in merge order.

The ordinary automatically loaded Linux config retains only mainBinaryName: procnote-gui; direct Rust checks therefore do not depend on generated package artifacts.

Safe Windows PATH migration

The old NSIS hook read and rewrote PATH using NSIS strings. Standard NSIS strings are limited to 1024 characters, so a sufficiently long user PATH could be truncated during installation or uninstallation.

The new hook delegates the registry transformation to Windows PowerShell and the .NET registry API. The helper:

  • reads the unexpanded registry value, preserving text such as %USERPROFILE%;
  • preserves whether PATH is REG_SZ or REG_EXPAND_SZ;
  • removes every exact $INSTDIR\bin and legacy $INSTDIR\cli entry case-insensitively;
  • does not remove substring neighbors such as $INSTDIR\bin-tools;
  • preserves unrelated entries, their order, and empty separators;
  • appends exactly one $INSTDIR\bin entry on installation;
  • removes both current and legacy entries on uninstallation; and
  • broadcasts WM_SETTINGCHANGE after the update.

During a v0.0.4 upgrade, the NSIS hook also removes the obsolete $INSTDIR\cli directory. This matters because Windows normally resolves .exe before .cmd through PATHEXT; leaving the legacy executable available could continue selecting the broken binary.

Windows runtime configuration

Windows MSVC builds enable Tauri's static versioned VC-runtime support and Cargo's crt-static target feature. The setting also applies to the new launcher.

The executables may dynamically use UCRT (UCRTBASE.DLL / API-MS-WIN-CRT-*), which is a Windows component, but artifact validation rejects dependencies on versioned Visual C++ Redistributable libraries such as VCRUNTIME, MSVCP, and CONCRT.

Windows execution persistence

A real-machine smoke test confirmed that the packaged launcher, --help, and --version worked, but clicking Start on a procedure returned Access is denied. (os error 5). The failed attempts left an empty .executions directory, while an independent write probe in the same procedure directory succeeded. That isolated the error to the durability step immediately after directory creation rather than workspace permissions, Controlled Folder Access, or the launcher.

ExecutionStore::create_execution() creates <procedure>/.executions and then calls sync_dir(). The previous Windows implementation opened that directory read-only with FILE_FLAG_BACKUP_SEMANTICS and called File::sync_all(). Rust implements that call with FlushFileBuffers on Windows; the API requires a write-capable handle and does not provide POSIX directory-fsync semantics, so the read-only directory handle returned ERROR_ACCESS_DENIED.

The Windows sync_dir() implementation is now intentionally a no-op. Unix platforms retain directory fsync, while actual data files—including event logs, execution snapshots, and attachments—continue to call File::sync_all() at their durable write points on every platform. Existing empty .executions directories are harmless and can be reused.

A focused syncing_existing_directory_succeeds regression test covers the helper. The Rust CI workflow now runs the persistence tests on native windows-latest, ensuring the original implementation would fail CI rather than reaching package testing again.

Artifact validation

The release workflow removes cached bundle directories before packaging so validators cannot accidentally inspect a stale artifact.

macOS

The validator:

  • mounts the produced DMG read-only;
  • verifies both executable locations and permissions;
  • compares the packaged launcher byte-for-byte with the freshly staged target binary;
  • checks GUI and launcher Mach-O architectures independently;
  • rejects WebKit linkage from the launcher;
  • captures real launcher --help and --version output when host-compatible;
  • confirms the GUI and launcher versions match; and
  • rejects the legacy CLI resource.

Both the arm64 build and x86_64 staging path are architecture-aware.

Linux

The validator:

  • extracts the Debian package;
  • verifies /usr/bin/procnote and /usr/bin/procnote-gui;
  • compares the packaged launcher with the freshly staged binary;
  • captures real help/version output and compares GUI/launcher versions;
  • checks shared-library resolution for both executables;
  • rejects GTK/WebKit linkage from the launcher; and
  • confirms that the desktop entry starts procnote-gui.

Windows

The native Windows validator inspects both NSIS and MSI payloads. It verifies:

  • exactly one GUI and one console launcher are packaged;
  • no shell-script launcher or legacy cli payload remains;
  • NSIS, MSI, and the freshly built launcher hashes agree;
  • the GUI is x64 and uses the Windows GUI subsystem;
  • the launcher is x64 and uses the Windows console subsystem;
  • the GUI manifest activates Microsoft.Windows.Common-Controls v6;
  • neither executable imports versioned Visual C++ Redistributable DLLs;
  • the launcher does not import COMCTL32.DLL;
  • the GUI can complete its startup-safe --version path; and
  • launcher help, version, and invalid-argument stdout/stderr/exit codes are correct, with no GUI process left behind.

It then exercises the real installer lifecycle:

  1. download the published v0.0.4 NSIS installer and verify its pinned SHA-256;
  2. install v0.0.4 silently and confirm the legacy CLI layout;
  3. replace the test PATH with a value longer than 1024 characters containing case-varied duplicates, substring neighbors, an unexpanded environment variable, and an empty entry;
  4. install the new NSIS package and verify exact migration;
  5. confirm launcher/helper hashes and removal of the legacy directory;
  6. simulate a fresh terminal PATH, resolve procnote to bin\procnote.exe, and capture its version output;
  7. uninstall and verify exact PATH cleanup; and
  8. restore the runner's original user PATH in a finally block.

The MSI is inspected through an administrative extraction, validating its payload without a normal machine installation.

Distribution behavior

Distribution Terminal command setup
Windows NSIS Native launcher plus automatic user-PATH entry
Windows MSI Native launcher included; PATH setup documented as manual
macOS Homebrew Automatic cask link to native launcher
macOS DMG Manual launcher symlink documented
Linux .deb / RPM /usr/bin/procnote installed automatically
Linux AppImage Manual launcher/PATH setup documented

Compatibility

This change does not alter procedure templates, execution state, or the append-only event-log schema. Windows no longer attempts the unsupported POSIX-style parent-directory sync; synchronization of actual event logs, snapshots, and attachments is unchanged.

Validation performed

  • just lint
    • Rust formatting and Clippy passed for the full workspace.
    • Svelte diagnostics passed with zero errors/warnings.
    • Generated TypeScript bindings remained in sync.
    • Vite+ passed with only the existing src/lib/index.ts empty-file warning.
  • cargo test --workspace passed from a clean launcher-staging state.
    • Includes launcher path-layout, argument-boundary, help/version/error, detachment, symlink, spaces, CWD, and environment-preservation tests.
    • Includes GUI parsing tests for the launcher's -- <workspace> contract.
    • Includes the directory-sync regression and the full execution-store creation path.
  • The new native windows-latest persistence-test job passed and is required by the aggregate Rust CI check.
  • cargo check --package procnote-launcher --target x86_64-pc-windows-msvc passed.
  • Windows PowerShell scripts parse successfully.
  • Shell, Node, JSON, and workflow YAML syntax checks passed.
  • An unsigned arm64 v0.0.5 DMG was built from an empty staging directory and passed package validation.
  • The x86_64 macOS launcher was cross-built and verified with lipo.
  • A v0.0.5 Debian package was built from an empty staging directory in a clean Linux container and passed package validation.

The native Windows persistence regression job now passes. The remaining release gate is the full NSIS/MSI lifecycle followed by repeating the packaged Start execution smoke test with the rebuilt installer. The v0.0.5 draft should not be published until those checks pass.

Suggested review order

  1. crates/procnote-launcher/ for the public CLI and process model.
  2. src-tauri/src/persistence/event_log.rs for the Windows directory-sync fix.
  3. src-tauri/launchers/README.md for package architecture and invariants.
  4. scripts/prepare-launcher.mjs, scripts/tauri.mjs, and tauri.bundle.*.conf.json for reproducible staging.
  5. src-tauri/nsis/hooks.nsh and update-user-path.ps1 for Windows migration.
  6. .github/workflows/*.yaml and scripts/validate-*-package.* for CI and release safeguards.

@shunichironomura
shunichironomura force-pushed the hotfix-terminal-launchers branch from f07b662 to 20686c6 Compare July 21, 2026 14:42
Tauri patches the main executable with its package type before building each
installer, so the NSIS and MSI copies always differ by the NSS/MSI marker.
Canonicalize that marker to UNK before hashing so the equivalence check still
proves both installers ship the same compiled GUI.
@shunichironomura shunichironomura self-assigned this Jul 22, 2026
@shunichironomura
shunichironomura merged commit f6ff017 into main Jul 22, 2026
20 of 32 checks passed
@shunichironomura
shunichironomura deleted the hotfix-terminal-launchers branch July 22, 2026 01:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant