Skip to content

feat: add Linux desktop installation support - #19

Closed
eltonacosta wants to merge 4 commits into
notmike101:mainfrom
eltonacosta:main
Closed

eltonacosta wants to merge 4 commits into
notmike101:mainfrom
eltonacosta:main

Conversation

@eltonacosta

Copy link
Copy Markdown

Summary

Adds Linux support to the ZCode Desktop Extensions host while preserving the existing Windows installation and update behavior.

The implementation supports ZCode running from a writable AppDir extracted from the official AppImage. Platform-specific paths and executable names are centralized so Windows continues to use the existing .exe binaries, Start Menu shortcut integration, and process detection.

Changes

  • Add platform-specific ZCode layouts and default installation paths.
  • Build zdp and zdp-launcher as executable ELF binaries on Linux.
  • Preserve the existing zdp.exe, zdp-launcher.exe, and ZCode.exe behavior on Windows.
  • Detect running Linux ZCode processes through /proc.
  • Add freedesktop .desktop integration while preserving the original desktop entry for uninstall.
  • Persist and reuse the configured ZCode installation path.
  • Forward desktop protocol URLs and additional launch arguments to ZCode.
  • Preserve, back up, rename, and restore app.asar.unpacked together with app.asar.
  • Launch the correct executable and guardian for each platform.
  • Use case-sensitive managed paths outside Windows.
  • Prevent Linux installations from applying the current Windows-only host update artifacts.
  • Make updater fixtures and task-service tests portable across Windows and Linux.

Windows Compatibility

Windows behavior remains unchanged:

  • The default installation remains under %LOCALAPPDATA%\Programs\ZCode.
  • Executables remain ZCode.exe, zdp.exe, and zdp-launcher.exe.
  • Start Menu .lnk integration continues to use PowerShell and WScript.Shell.
  • Running-process detection continues to use tasklist.exe.
  • --windows-hide-console remains enabled for the Windows launcher build.
  • Existing installation state remains compatible because the Linux desktop-entry state field is optional.
  • Packaged host self-updates remain enabled on Windows.

Linux Installation Model

AppImages are mounted read-only and cannot be patched in place. Linux installation therefore targets a writable AppDir extracted from the official ZCode AppImage.

The default Linux paths are:

  • ZCode AppDir: ~/.local/opt/ZCode
  • Desktop entry: $XDG_DATA_HOME/applications/zcode.desktop, falling back to ~/.local/share/applications/zcode.desktop

The original AppImage can remain installed as an unmodified recovery source.

Host self-update is intentionally marked non-installable on Linux until platform-specific release artifacts and feeds are available. Extension updates remain unaffected.

Validation

Tested on Linux x86-64 with ZCode 3.11.2 extracted from the official AppImage.

bun run check
54 pass
0 fail
The complete validation includes:
- TypeScript typecheck
- Full test suite
- SDK build and package verification
- Example extension build
- Host and renderer runtime build
- Linux CLI and launcher compilation
- Install, repair, launch, doctor, and guardian verification
- Native unpacked module preservation
- Renderer reaching the renderer-ready state

Copilot AI lite review requested due to automatic review settings September 13, 2026 21:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved installation, platform handling, test portability, and Linux CI issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds Linux AppDir installation and launch support for ZCode Desktop Extensions while preserving Windows behavior.

Changes:

  • Adds platform-specific paths, executables, process detection, and launch handling.
  • Adds Linux desktop-entry integration and installation-path persistence.
  • Updates native resource preservation, builds, updater behavior, and cross-platform tests.
File summaries
File Reviewed change
tests/task-service.test.ts Uses portable workspace paths.
tests/installer.test.ts Covers platform layouts and unpacked resources.
tests/host-updater.test.ts Adds portable updater fixtures.
tests/host-update-apply.test.ts Tests platform-specific launch paths.
tests/extension-updater.test.ts Updates portable archive handling.
src/shared/schemas.ts Stores optional desktop-entry state.
src/shared/constants.ts Centralizes platform paths and executables.
src/host/host-updater.ts Handles platform-aware host updates.
src/host/bootstrap.ts Launches the platform-specific guardian.
src/cli/shortcut.ts Adds Linux desktop-entry integration.
src/cli/launcher.ts Launches the platform-specific helper.
src/cli/installer.ts Adds Linux installation and resource preservation.
src/cli/index.ts Persists installation paths and forwards arguments.
src/cli/host-update-apply.ts Applies platform-aware update launches.
src/cli/guardian.ts Performs platform-aware process checks.
scripts/build.ts Builds platform-specific executables.
Review details

Suppressed comments (5)

scripts/build.ts:58

  • The new Linux-specific build path is not exercised by CI: .github/workflows/ci.yml still runs only on windows-latest. That leaves the ELF build, Linux process detection, desktop-entry integration, and --no-sandbox launch path without automated regression coverage; add a Linux job or matrix entry for the check suite.
  "build", path.join(root, "src", "cli", "index.ts"), "--compile", "--minify", "--sourcemap", "--outfile", path.join(bin, ZDP_EXECUTABLE),
]);
const launcherArgs = ["build", path.join(root, "src", "cli", "launcher.ts"), "--compile", "--minify"];
if (process.platform === "win32") launcherArgs.push("--windows-hide-console");
launcherArgs.push("--outfile", path.join(bin, ZDP_LAUNCHER_EXECUTABLE));
await runBunBuild(launcherArgs);
if (process.platform !== "win32") await Promise.all([
  chmod(path.join(bin, ZDP_EXECUTABLE), 0o755),
  chmod(path.join(bin, ZDP_LAUNCHER_EXECUTABLE), 0o755),

src/cli/index.ts:10

  • --zcode is persisted and reused exactly as supplied, so a relative value such as --zcode ./ZCode is stored relative to the install command's working directory. The launcher later starts zdp with the ZDP root as its cwd, making the same value resolve to a different directory (and launch can try to execute <root>/ZCode/zcode from <root>/ZCode). Normalize the ZCode path to an absolute path before storing and using it.
const zcodeRoot = valueAfter("--zcode") ?? installed?.zcodeRoot ?? DEFAULT_ZCODE_ROOT;

src/cli/shortcut.ts:16

  • If XDG_DATA_HOME is set to an empty string, nullish coalescing selects "" and this produces the relative path applications/zcode.desktop instead of the XDG fallback. Empty is treated like unset for this variable, so use a truthy/trimmed check before falling back to ~/.local/share.
  if (process.platform !== "win32") return path.join(process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share"), "applications", "zcode.desktop");

src/host/host-updater.ts:85

  • This deliberately makes Linux host updates non-installable, but the renderer uses installable === false to display that the installation is a development checkout. A packaged writable Linux AppDir will therefore show a false explanation for every available host update; expose a platform-specific reason/status or update the UI copy for Linux.
    const installable = process.platform === "win32" && await this.#installedManifest().then(() => true).catch(() => false);

src/shared/constants.ts:16

  • These conditionals treat every non-Windows platform as Linux. On macOS or another unsupported platform this selects zdp/zcode, the Linux install directory, and --no-sandbox, while isZCodeRunning explicitly returns false there, so commands can silently attempt an invalid Linux installation instead of rejecting the platform. Use an explicit Linux branch and fail fast for unsupported platforms.
export const ZDP_EXECUTABLE = process.platform === "win32" ? "zdp.exe" : "zdp";
export const ZDP_LAUNCHER_EXECUTABLE = process.platform === "win32" ? "zdp-launcher.exe" : "zdp-launcher";
export const DEFAULT_ZCODE_ROOT = process.platform === "win32"
  ? path.join(process.env.LOCALAPPDATA ?? "C:\\Users\\me\\AppData\\Local", "Programs", "ZCode")
  : path.join(os.homedir(), ".local", "opt", "ZCode");

export function getZCodeLayout(root: string) {
  const electronExecutable = path.join(root, process.platform === "win32" ? "ZCode.exe" : "zcode");
  • Files reviewed: 16/16 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

const child = Bun.spawn(["powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command], {
const command = process.platform === "win32"
? ["powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", `Compress-Archive -LiteralPath ${quote(source)} -DestinationPath ${quote(destination)} -CompressionLevel Optimal -Force`]
: ["bsdtar", "-a", "-cf", destination, path.basename(source)];
Comment thread src/cli/shortcut.ts
Comment on lines +55 to +59
const existing = await readFile(shortcutPath, "utf8").catch(() => "[Desktop Entry]\nName=ZCode\nType=Application\nTerminal=false\nCategories=Development;\nMimeType=x-scheme-handler/zcode;\n");
const exec = `Exec=${quoteDesktopValue(launcher)} %U`;
const tryExec = `TryExec=${launcher}`;
let contents = /^Exec=.*$/m.test(existing) ? existing.replace(/^Exec=.*$/m, exec) : `${existing.trimEnd()}\n${exec}\n`;
contents = /^TryExec=.*$/m.test(contents) ? contents.replace(/^TryExec=.*$/m, tryExec) : `${contents.trimEnd()}\n${tryExec}\n`;
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.

2 participants