From ba6ddbab1d3cc0fce2d5521e26dc2ec9e0ef6ea5 Mon Sep 17 00:00:00 2001 From: wuqingwei Date: Mon, 26 Oct 2026 19:12:29 +0800 Subject: [PATCH 1/4] feat(linux): initial Linux port scaffolding Browser via xdg-open, clipboard via wl-copy/xclip/xsel, file actions via xdg-open, SSH prompts via zenity, Linux native addon build (.so) and tarball packaging. See LINUX-PORT.md for the remaining GUI verification on Ubuntu 20.04. --- .changeset/linux-port-initial.md | 5 + LINUX-PORT.md | 142 ++++++++++++++ apps/gpuix/native/build.ts | 18 +- apps/gpuix/native/package-linux.ts | 181 ++++++++++++++++++ apps/gpuix/native/package-resources.ts | 28 +++ apps/gpuix/package.json | 1 + .../editor-settings/external-editors.ts | 16 +- .../src/feature/machines/ssh-connection.ts | 33 ++++ .../lib/repository-file-actions.ts | 33 +++- apps/gpuix/src/platform/browser.ts | 25 ++- apps/gpuix/src/platform/clipboard.ts | 43 ++++- apps/gpuix/src/window-options.ts | 18 +- package.json | 1 + 13 files changed, 507 insertions(+), 37 deletions(-) create mode 100644 .changeset/linux-port-initial.md create mode 100644 LINUX-PORT.md create mode 100644 apps/gpuix/native/package-linux.ts diff --git a/.changeset/linux-port-initial.md b/.changeset/linux-port-initial.md new file mode 100644 index 00000000..44573e24 --- /dev/null +++ b/.changeset/linux-port-initial.md @@ -0,0 +1,5 @@ +--- +"@1git/gpuix": minor +--- + +Add an initial Linux port: build the patched GPUIX addon on Linux, open browsers and files with xdg-open, copy with wl-copy/xclip/xsel, prompt SSH host trust and passwords with zenity, and package a self-contained Linux tarball with `bun run package:linux`. macOS behaviour is unchanged. diff --git a/LINUX-PORT.md b/LINUX-PORT.md new file mode 100644 index 00000000..9ef88f37 --- /dev/null +++ b/LINUX-PORT.md @@ -0,0 +1,142 @@ +# Linux port (work in progress) + +Status: scaffolding landed on branch `linux-port`, commit `feat(linux)`. +This is **not** a finished, runnable Linux app yet. The TypeScript side +builds and type-checks on Linux; the native GUI build has not been run +to a window yet (see "Not yet verified"). + +## Why this port is plausible + +The hard part is not the UI engine. 1git is TypeScript + React 19 on +GPUIX (Zed's GPUI bindings), and GPUI itself already has Linux X11 and +Wayland backends. Evidence in this repo: + +- `bun.lock` already pins `@gpuix/native-linux-x64-gnu 0.10.0` as an + optional dependency of `@gpuix/native 0.10.0`. +- GPUIX upstream ships Linux examples and AppImage packaging via + `cargo-packager`. +- The SSH remote helper already builds static Linux glibc/musl binaries + (`apps/gpuix/native/build-remote-helpers.ts`), and the `packages/git` + layer is plain Git subprocess work, not macOS code. +- Session and avatar-cache paths already fall through to + `XDG_STATE_HOME` / `XDG_CACHE_HOME` on non-macOS platforms. + +The blockers were 1git's own macOS gates, not the engine: + +- `apps/gpuix/native/build.ts` threw unless `darwin/arm64`. +- `src/platform/browser.ts` / `clipboard.ts` threw unless macOS + (`/usr/bin/open`, `pbcopy`). +- File actions, editor launching and SSH prompts hard-coded + `/usr/bin/open`, `/usr/bin/open -a`, and `osascript` dialogs. +- Packaging, signing and updates were macOS-only (`.app`, codesign, + Sparkle, Keychain copy in UI strings). + +## What this branch changes + +- `native/build.ts`: allow Linux x64/arm64; copy `libgpuix_native.so` + instead of `.dylib`. The `*-platform` patches that target + `crates/gpui_macos` still apply (those files exist in the checkout) + but are not compiled into the Linux addon, so the patch stack is not + forked. +- `native/package-linux.ts` + `package:linux` scripts: a self-contained + tarball (`build/linux/1git-linux-.tar.gz`) reusing the macOS + bundle's relative layout (`bin/1git`, `Frameworks/gpuix-native.node`, + `Resources/app.js`, `Resources/remote-helpers`), so + `native/packaged-main.ts` works unchanged. No signing, no Sparkle; + `AppUpdater.start()` already no-ops off macOS. +- `native/package-resources.ts`: `copyImageRuntimeLinux()` copies the + glibc sharp runtime (`@img/sharp-linux-`, + `@img/sharp-libvips-linux-`), not the darwin one and not musl. +- `src/platform/browser.ts`: `xdg-open` on Linux. +- `src/platform/clipboard.ts`: `wl-copy`, then `xclip`, then `xsel`. + Note: `Bun.spawn` throws when a binary is missing instead of exiting + non-zero, so each candidate is wrapped in try/catch (found by a + smoke test, see below). +- `src/feature/repository-workspace/lib/repository-file-actions.ts`: + open/reveal via `xdg-open` (reveal opens the parent folder; xdg-open + has no `-R` equivalent). +- `src/feature/machines/ssh-connection.ts`: SSH host-trust/password + prompts via `zenity` on Linux instead of `osascript`. +- `src/feature/editor-settings/external-editors.ts`: on Linux, an + explicit executable path is run directly, otherwise `xdg-open`. + Editor catalog discovery is still macOS-only (see TODO). +- `src/window-options.ts`: no blurred background or traffic lights on + Linux; the compositor's native decorations are used. +- Changeset: `.changeset/linux-port-initial.md` (`@1git/gpuix`, minor). + +## Ubuntu 20.04: the floor, and the build rule + +Target: Ubuntu 20.04 LTS x64, glibc 2.31, Git 2.25, X11 by default +(Wayland session optional). + +**Build on 20.04 to ship for 20.04.** glibc is forward-compatible +only: a Rust/Bun binary built on Ubuntu 24.04 (glibc 2.39) will refuse +to run on 20.04. Do the release build in a 20.04 container or on a +20.04 host, and verify with `ldd` that no `GLIBC_2.32+` symbol is +required. This VM (Ubuntu 24.04) is therefore fine for development +checks, but not for a 20.04 release binary. + +Other 20.04 notes to verify on a real 20.04 machine (not assumed here): + +- Bun 1.4.2: its Linux binary is known to require at least + GLIBC_2.29, which 2.31 satisfies, but the exact 1.4.2 binary must + still be run on 20.04 before claiming support. +- Sharp 0.35.4 linux-x64 (glibc) prebuilt runtime: check it loads on + 2.31; do not substitute the linuxmusl build on Ubuntu. +- Git 2.25 covers `switch`/`restore`/worktrees; audit any newer Git + flag in `packages/git` before release. +- `curl --unix-socket` (used by the SSH askpass script) exists in + 20.04's curl 7.68. `zenity` and `xdg-utils` are the new runtime + dependencies to document, not bundle. + +Runtime dependencies for the tarball (to install, not bundle): +`git`, `xdg-utils`, `zenity`, `wl-clipboard` (Wayland) or `xclip` +(X11), plus the desktop's Wayland/X11 libraries and a Vulkan driver +(or Mesa lavapipe for software rendering). + +## Verified in this session (Ubuntu 24.04, no display, no Rust) + +- `bun install --frozen-lockfile`: ok (616 packages). +- `bun run build` (turbo, all 5 tasks): ok. +- `bun run --cwd apps/gpuix check-types`: ok after the workspace build. +- `bun x oxfmt --check` and `bun x oxlint` on the changed files: clean. +- Shim smoke test (Bun, no display): invalid URL still rejected; + clipboard on a machine with no wl-copy/xclip/xsel now fails with the + intended "install wl-clipboard or xclip" message instead of a raw + spawn error. + +## Not yet verified / TODO + +1. **Native build and first window.** Install Rust (GPUIX pins + 1.97.1 in its `rust-toolchain.toml`) and the GPUI Linux system + libraries, then run `bun run --cwd apps/gpuix build:native` and + `bun run dev:app` under Xvfb and then on a real GNOME X11 and + Wayland session. Expect Vulkan/fontconfig issues to surface here + first. This is the main remaining risk. +2. **Cmd -> Ctrl sweep.** Many handlers already accept `cmd || ctrl`, + but pure `cmd` checks (quick switcher, Cmd-click, settings + shortcuts) will not fire on Linux, where GPUI reports the Super key + as `cmd`. Map them to Ctrl on Linux, file by file, with tests. +3. **Editor catalog on Linux.** Discover `code`, `cursor`, `zed`, + `subl` from PATH and `.desktop` files; the current Linux branch + only handles an explicit executable path or the default handler. +4. **File-menu labels and icons.** "Reveal in Finder" and the + Finder.app icon path are still macOS strings; rename to the file + manager on Linux and drop the Finder icon lookup. +5. **Credentials.** Account secrets go through GPUI's + `read/write_credentials` (added by `accounts.patch`). Verify it + against GNOME Keyring and KWallet on Linux, and decide the + fallback for headless/no-keyring machines. Do not silently fall + back to plaintext. +6. **Updater.** Sparkle is macOS-only. Either ship Linux with no + auto-updater (v1, documented in the tarball README) or adopt + GPUIX's GitHub-Releases `checkUpdate` flow. +7. **Packaging.** The tarball is a stopgap. A `.deb` (or AppImage via + `cargo-packager`, as GPUIX upstream does) with a real `.desktop` + file and icon is the proper distribution format. +8. **Tests.** The vibrancy, traffic-light, signing, notarization and + macOS launch integration suites are macOS-only; they need Linux + equivalents or explicit skips, not silent passes. +9. **Push decision.** This branch lives only in this workspace. + Nothing has been pushed to `1bye/1git` and no PR has been opened; + pushing upstream or forking is a separate decision for the user. diff --git a/apps/gpuix/native/build.ts b/apps/gpuix/native/build.ts index 917ca02d..e0afacdf 100644 --- a/apps/gpuix/native/build.ts +++ b/apps/gpuix/native/build.ts @@ -55,12 +55,21 @@ import { systemAppearancePlatformPatchPath, } from "./config"; -if (process.platform !== "darwin" || process.arch !== "arm64") { +const supportedNativeBuild = + (process.platform === "darwin" && process.arch === "arm64") || + (process.platform === "linux" && ["x64", "arm64"].includes(process.arch)); + +if (!supportedNativeBuild) { throw new Error( - "The local GPUIX build currently supports Apple Silicon macOS only." + "The local GPUIX build currently supports Apple Silicon macOS and Linux x64/arm64 only." ); } +// The *-platform patches that target crates/gpui_macos still apply cleanly +// on Linux (those source files exist in the checkout); they are simply not +// compiled into the Linux addon. Keeping the full patch stack avoids a +// second, diverging patch set for Linux. + const source = resolve(appDirectory, ".cache/native-source"); const run = async (cmd: string[], cwd = source): Promise => { @@ -229,7 +238,10 @@ if ( resolve(source, "packages/native") ); await copyFile( - resolve(source, "packages/native/target/release/libgpuix_native.dylib"), + resolve( + source, + `packages/native/target/release/libgpuix_native.${process.platform === "darwin" ? "dylib" : "so"}` + ), `${nativeLibrary}.tmp` ); await rename(`${nativeLibrary}.tmp`, nativeLibrary); diff --git a/apps/gpuix/native/package-linux.ts b/apps/gpuix/native/package-linux.ts new file mode 100644 index 00000000..96a8b6b4 --- /dev/null +++ b/apps/gpuix/native/package-linux.ts @@ -0,0 +1,181 @@ +import { + chmod, + copyFile, + cp, + mkdir, + mkdtemp, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import { resolve } from "node:path"; +import { createHash } from "node:crypto"; +import { appDirectory, nativeLibrary } from "./config"; +import { copyImageRuntimeLinux } from "./package-resources"; +import { version as developmentVersion } from "../package.json"; +import { ReleaseVersion } from "./release-options"; +import { remoteHelperTargets } from "../src/feature/machines/remote-protocol"; + +/** + * Linux packaging: a self-contained tarball, not a .deb or AppImage yet. + * Layout mirrors the macOS bundle's relative paths so packaged-main.ts + * works unchanged: the compiled executable lives in bin/, the native + * addon in Frameworks/, and the JS app plus remote helpers in Resources/. + * No signing or Sparkle updater: on Linux there is no Sparkle, and + * AppUpdater.start() already no-ops off macOS. + * + * Build the tarball on Ubuntu 20.04 (or in a 20.04 container) to keep + * glibc 2.31 as the ceiling. A binary built on Ubuntu 24.04 (glibc 2.39) + * will not run on 20.04. + */ +if (process.platform !== "linux" || !["x64", "arm64"].includes(process.arch)) { + throw new Error("Linux packaging currently supports Linux x64/arm64 only."); +} + +const arch = process.arch === "x64" ? "x64" : "arm64"; + +const run = async (command: string[]): Promise => { + const child = Bun.spawn(command, { + cwd: appDirectory, + stdout: "inherit", + stderr: "inherit", + }); + + if ((await child.exited) !== 0) + throw new Error(`Linux packaging failed: ${command.join(" ")}`); +}; + +const version = ReleaseVersion.parse( + process.env.ONEGIT_RELEASE_VERSION ?? developmentVersion +); + +const output = resolve(appDirectory, "build/linux"); + +await mkdir(output, { recursive: true }); +const staging = await mkdtemp(resolve(output, ".package-")); +const root = resolve(staging, "1git"); +const resources = resolve(root, "Resources"); +const executable = resolve(root, "bin/1git"); +const addon = resolve(root, "Frameworks/gpuix-native.node"); +const archiveName = `1git-linux-${arch}.tar.gz`; +const archive = resolve(staging, archiveName); + +try { + await mkdir(resources, { recursive: true }); + await mkdir(resolve(root, "bin")); + await mkdir(resolve(root, "Frameworks")); + + await run([ + process.execPath, + "build", + "src/main.tsx", + "--target=bun", + "--production", + "--define", + `process.env.ONEGIT_APP_VERSION=${JSON.stringify(version)}`, + "--external=sharp", + "--outfile", + resolve(resources, "app.js"), + ]); + + await run([ + process.execPath, + "build", + "native/packaged-main.ts", + "--compile", + "--compile-autoload-package-json", + "--no-compile-autoload-dotenv", + "--no-compile-autoload-bunfig", + "--outfile", + executable, + ]); + await chmod(executable, 0o755); + await copyFile(nativeLibrary, addon); + await copyFile( + resolve(appDirectory, "../../LICENSE"), + resolve(resources, "1git-LICENSE.txt") + ); + await copyFile( + resolve(appDirectory, "../../THIRD_PARTY_NOTICES.md"), + resolve(resources, "THIRD_PARTY_NOTICES.md") + ); + await cp(resolve(appDirectory, "LICENSES"), resolve(resources, "LICENSES"), { + recursive: true, + }); + await copyFile( + resolve( + appDirectory, + "src/feature/repository-appearance/pattern-artwork.LICENSE" + ), + resolve(resources, "RepositoryPatterns.LICENSE") + ); + await copyImageRuntimeLinux(resources, arch); + await run([process.execPath, "run", "native/build-remote-helpers.ts"]); + await mkdir(resolve(resources, "remote-helpers")); + + for (const target of remoteHelperTargets) { + for (const extension of ["gz", "json"]) { + await copyFile( + resolve(appDirectory, `build/remote-helpers/${target}.${extension}`), + resolve(resources, `remote-helpers/${target}.${extension}`) + ); + } + } + + await writeFile( + resolve(root, "1git.desktop"), + `[Desktop Entry] +Type=Application +Name=1git +Comment=A native Git workspace +Exec=1git +Icon=1git +Categories=Development;RevisionControl; +Terminal=false +` + ); + await writeFile( + resolve(root, "README.txt"), + `1git ${version} - Linux beta + +Requirements: Ubuntu 20.04 or later (glibc 2.31+), x86_64 or ARM64, +a Wayland or X11 session, Git, and xdg-utils. For SSH host prompts, +install zenity. For clipboard, install wl-clipboard (Wayland) or +xclip (X11). Bun, Rust and Xcode are not needed to run this app. + +Install: +1. Extract this archive, e.g. to /opt/1git. +2. Run /opt/1git/bin/1git, or symlink it into /usr/local/bin/1git. +3. Optional: copy 1git.desktop to ~/.local/share/applications/ and + set Exec to the full path of bin/1git. + +No auto-updater is included in this Linux build. Accounts and +preferences are stored separately on your machine, not in this archive. +AI providers receive checked changes only when you choose to generate +a message. +` + ); + + await run(["tar", "-czf", archive, "-C", staging, "1git"]); + const hash = createHash("sha256"); + + for await (const chunk of Bun.file(archive).stream()) hash.update(chunk); + + await writeFile( + resolve(staging, `${archiveName}.sha256`), + `${hash.digest("hex")} ${archiveName}\n` + ); + + await rm(resolve(output, "1git"), { recursive: true, force: true }); + await rename(root, resolve(output, "1git")); + await rename(archive, resolve(output, archiveName)); + await rename( + resolve(staging, `${archiveName}.sha256`), + resolve(output, `${archiveName}.sha256`) + ); + process.stdout.write( + `Built ${resolve(output, "1git")}\nShare ${resolve(output, archiveName)}\n` + ); +} finally { + await rm(staging, { recursive: true, force: true }); +} diff --git a/apps/gpuix/native/package-resources.ts b/apps/gpuix/native/package-resources.ts index 8a5a309c..a417f1d0 100644 --- a/apps/gpuix/native/package-resources.ts +++ b/apps/gpuix/native/package-resources.ts @@ -30,6 +30,34 @@ export const copyImageRuntime = async (resources: string): Promise => { } }; +/** Linux counterpart of copyImageRuntime: the glibc (not musl) sharp runtime + * for the build host's architecture. Ubuntu 20.04 is glibc, so the + * linuxmusl packages must not be substituted here. */ +export const copyImageRuntimeLinux = async ( + resources: string, + arch: string +): Promise => { + const linuxPackages = [ + "sharp", + `@img/sharp-linux-${arch}`, + `@img/sharp-libvips-linux-${arch}`, + "@img/colour", + "detect-libc", + "semver", + ] as const; + + for (const name of linuxPackages) { + const manifest = sharpRequire.resolve(`${name}/package.json`); + const destination = resolve(resources, "node_modules", name); + + await mkdir(dirname(destination), { recursive: true }); + await cp(dirname(manifest), destination, { + recursive: true, + dereference: true, + }); + } +}; + export const bundleInfoPlist = ( version: string, updates?: UpdateConfiguration diff --git a/apps/gpuix/package.json b/apps/gpuix/package.json index ff5d59b1..aefe12c9 100644 --- a/apps/gpuix/package.json +++ b/apps/gpuix/package.json @@ -10,6 +10,7 @@ "setup:signing:macos": "bun native/setup-signing.ts", "test:signing:macos": "bun test tests/packaging/signing.integration.test.ts tests/packaging/hardened-runtime.integration.test.ts", "package:macos": "bun native/package-macos.ts", + "package:linux": "bun native/package-linux.ts", "test:package:macos": "bun test tests/packaging/launch.integration.test.ts", "test:updates:macos": "bun test tests/updater/install.integration.test.ts tests/updater/lifecycle.integration.test.ts tests/updater/preparation.integration.test.ts", "prepare:updates:macos": "bun native/prepare-updates.ts", diff --git a/apps/gpuix/src/feature/editor-settings/external-editors.ts b/apps/gpuix/src/feature/editor-settings/external-editors.ts index a957dbb5..3c83b223 100644 --- a/apps/gpuix/src/feature/editor-settings/external-editors.ts +++ b/apps/gpuix/src/feature/editor-settings/external-editors.ts @@ -515,9 +515,6 @@ export class ExternalEditors { /** Re-read the preference for each explicit file open. An unavailable choice fails instead of silently launching another app. */ async open(absolute: string, override?: string | null): Promise { - if (process.platform !== "darwin") - throw new Error("Open in editor currently requires macOS."); - if ( !isAbsolute(absolute) || (await realpath(absolute)) !== absolute || @@ -527,6 +524,19 @@ export class ExternalEditors { "Only existing regular files can be opened in an editor." ); + // Linux has no .app catalog yet (see LINUX-PORT.md). An explicit + // executable path is run directly; otherwise the default handler + // (xdg-open) is used. Catalog discovery for code/cursor/zed is TODO. + if (process.platform === "linux") { + if (override) await this.run(override, [absolute]); + else await this.run("xdg-open", [absolute]); + + return; + } + + if (process.platform !== "darwin") + throw new Error("Open in editor currently requires macOS."); + const selected = override === undefined ? ExternalEditors.resolve(absolute, this.preferences()) diff --git a/apps/gpuix/src/feature/machines/ssh-connection.ts b/apps/gpuix/src/feature/machines/ssh-connection.ts index 73ba8412..29676dee 100644 --- a/apps/gpuix/src/feature/machines/ssh-connection.ts +++ b/apps/gpuix/src/feature/machines/ssh-connection.ts @@ -133,6 +133,39 @@ export class SshConnection { if (saved) return saved; + // Linux has no osascript. zenity (GNOME, usually preinstalled on + // Ubuntu) provides the equivalent out-of-process dialog; a missing + // zenity or a cancelled dialog fails the SSH attempt, it never + // silently falls back to another credential source. + if (process.platform === "linux") { + if (confirmation) { + await execute( + "zenity", + ["--question", "--title=1git SSH", `--text=${text}`], + { + signal: this.controller.signal, + maxBuffer: 16384, + } + ); + + return "yes"; + } + + const result = await execute( + "zenity", + ["--password", "--title=1git SSH", `--text=${text}`], + { + signal: this.controller.signal, + maxBuffer: 16384, + } + ); + + return result.stdout.replace(/\r?\n$/, ""); + } + + if (process.platform !== "darwin") + throw new Error("SSH prompts are not supported on this platform."); + const script = confirmation ? 'on run argv\ndisplay dialog (item 1 of argv) buttons {"Cancel", "Trust host"} default button "Cancel" with title "1git SSH"\nreturn "yes"\nend run' : 'on run argv\nset answer to display dialog (item 1 of argv) default answer "" with hidden answer buttons {"Cancel", "Connect"} default button "Connect" with title "1git SSH"\nreturn text returned of answer\nend run'; diff --git a/apps/gpuix/src/feature/repository-workspace/lib/repository-file-actions.ts b/apps/gpuix/src/feature/repository-workspace/lib/repository-file-actions.ts index c52acf32..2925fe26 100644 --- a/apps/gpuix/src/feature/repository-workspace/lib/repository-file-actions.ts +++ b/apps/gpuix/src/feature/repository-workspace/lib/repository-file-actions.ts @@ -29,6 +29,26 @@ import { ExternalEditors } from "../../editor-settings/external-editors"; import type { CommitFileSelection } from "../types"; const execute = promisify(execFile); + +/** Open a file or folder with the OS default handler. No shell interpolation. */ +async function openWithDefaultApp(absolute: string): Promise { + if (process.platform === "darwin") + await execute("/usr/bin/open", [absolute], { timeout: 10_000 }); + else if (process.platform === "linux") + await execute("xdg-open", [absolute], { timeout: 10_000 }); + else throw new Error("Opening files is not supported on this platform."); +} + +/** Reveal a path in the OS file manager. xdg-open has no -R equivalent, so + * on Linux opening the parent folder is the closest match. */ +async function revealInFileManager(absolute: string): Promise { + if (process.platform === "darwin") + await execute("/usr/bin/open", ["-R", absolute], { timeout: 10_000 }); + else if (process.platform === "linux") + await execute("xdg-open", [join(absolute, "..")], { timeout: 10_000 }); + else throw new Error("Revealing files is not supported on this platform."); +} + const nullObjectId = /^0+$/u; const objectId = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/iu; @@ -103,7 +123,7 @@ export class RepositoryFileActions { ); if (kind === "editor") await editors.open(absolute, application); - else await execute("/usr/bin/open", [absolute], { timeout: 10_000 }); + else await openWithDefaultApp(absolute); } catch (cause) { await rm(directory, { recursive: true, force: true }); @@ -121,7 +141,7 @@ export class RepositoryFileActions { const path = await realpath(directory); - await execute("/usr/bin/open", ["-R", path], { timeout: 10_000 }); + await openWithDefaultApp(path); } static async revealRecovery(directory: string) { @@ -130,7 +150,7 @@ export class RepositoryFileActions { `Recovery files are on your SSH machine: ${MachineLocation.display(directory)}` ); - await execute("/usr/bin/open", [directory], { timeout: 10_000 }); + await openWithDefaultApp(directory); } static async open( root: string, @@ -197,11 +217,8 @@ export class RepositoryFileActions { "Only existing regular files inside the repository can be opened or revealed." ); - await execute( - "/usr/bin/open", - kind === "finder" ? ["-R", absolute] : [absolute], - { timeout: 10_000 } - ); + if (kind === "finder") await revealInFileManager(absolute); + else await openWithDefaultApp(absolute); } static async ignore(root: string, request: FileIgnoreRequest) { diff --git a/apps/gpuix/src/platform/browser.ts b/apps/gpuix/src/platform/browser.ts index 1da5938e..a33f9fcf 100644 --- a/apps/gpuix/src/platform/browser.ts +++ b/apps/gpuix/src/platform/browser.ts @@ -12,15 +12,26 @@ export class NativeBrowser { if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new Error("Invalid browser URL."); - if (process.platform !== "darwin") - throw new Error("Opening the browser is currently supported on macOS."); + const command = + process.platform === "darwin" + ? ["/usr/bin/open", url.href] + : process.platform === "linux" + ? ["xdg-open", url.href] + : null; - const child = Bun.spawn(["/usr/bin/open", url.href], { - stdout: "ignore", - stderr: "ignore", - }); + if (!command) + throw new Error("Opening the browser is not supported on this platform."); - if ((await child.exited) !== 0) + try { + const child = Bun.spawn(command, { + stdout: "ignore", + stderr: "ignore", + }); + + if ((await child.exited) !== 0) + throw new Error("Could not open the browser. Try again."); + } catch { throw new Error("Could not open the browser. Try again."); + } } } diff --git a/apps/gpuix/src/platform/clipboard.ts b/apps/gpuix/src/platform/clipboard.ts index ee3e1f72..446dc96e 100644 --- a/apps/gpuix/src/platform/clipboard.ts +++ b/apps/gpuix/src/platform/clipboard.ts @@ -1,16 +1,39 @@ -/** Shared macOS text clipboard boundary. Values travel through stdin, never shell commands. */ +/** Shared text clipboard boundary. Values travel through stdin, never shell commands. */ export class NativeClipboard { static async write(value: string): Promise { - if (process.platform !== "darwin") - throw new Error("Clipboard copying is currently supported on macOS."); + // Wayland first, then X11. Each candidate is tried in order; a missing + // binary exits non-zero and falls through to the next one. + const commands = + process.platform === "darwin" + ? [["/usr/bin/pbcopy"]] + : process.platform === "linux" + ? [ + ["wl-copy"], + ["xclip", "-selection", "clipboard"], + ["xsel", "--clipboard", "--input"], + ] + : []; - const child = Bun.spawn(["/usr/bin/pbcopy"], { - stdin: new TextEncoder().encode(value), - stdout: "ignore", - stderr: "ignore", - }); + if (!commands.length) + throw new Error("Clipboard copying is not supported on this platform."); - if ((await child.exited) !== 0) - throw new Error("Could not copy to the clipboard."); + for (const command of commands) { + try { + const child = Bun.spawn(command, { + stdin: new TextEncoder().encode(value), + stdout: "ignore", + stderr: "ignore", + }); + + if ((await child.exited) === 0) return; + } catch { + // Bun.spawn throws when the binary is missing; try the next one. + continue; + } + } + + throw new Error( + "Could not copy to the clipboard. On Linux, install wl-clipboard (Wayland) or xclip (X11)." + ); } } diff --git a/apps/gpuix/src/window-options.ts b/apps/gpuix/src/window-options.ts index 87434fe6..ff8ae52d 100644 --- a/apps/gpuix/src/window-options.ts +++ b/apps/gpuix/src/window-options.ts @@ -10,7 +10,9 @@ import { handleRepositoryFilterKeyDown } from "./feature/repository-workspace/li import { handleWorkspaceKeyDown } from "./feature/repository-workspace/lib/repository-filter-shortcut"; import { handleCloneNavigation } from "./feature/repository-workspace/lib/repository-filter-shortcut"; -/** Native window chrome and keyboard traversal; application content stays transparent around the workspace. */ +/** Native window chrome and keyboard traversal. On Linux there is no AppKit + * vibrancy or traffic-light chrome, so the window keeps the compositor's + * native decorations and an opaque background instead of the macOS look. */ export const windowOptions: RenderOptions = { height: 820, width: 1280, @@ -18,11 +20,15 @@ export const windowOptions: RenderOptions = { minWidth: 640, minHeight: 420, title: "1git", - titlebarTransparent: true, - appOwnsTitlebarDrag: true, - windowBackground: "blurred", - trafficLightX: 16, - trafficLightY: 13, + ...(process.platform === "darwin" + ? { + titlebarTransparent: true, + appOwnsTitlebarDrag: true, + windowBackground: "blurred", + trafficLightX: 16, + trafficLightY: 13, + } + : {}), onKeyDown(event, renderer) { if (handlePopoverKeyDown(event, renderer)) return; diff --git a/package.json b/package.json index c20bb7a7..d58146b7 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "build:web": "turbo run build --filter=@1git/web", "build": "turbo run build", "package:macos": "turbo run package:macos --filter=@1git/gpuix", + "package:linux": "turbo run package:linux --filter=@1git/gpuix", "setup:signing:macos": "turbo run setup:signing:macos --filter=@1git/gpuix", "test:signing:macos": "turbo run test:signing:macos --filter=@1git/gpuix", "test:package:macos": "turbo run test:package:macos --filter=@1git/gpuix", From eac653a2eeb391455b93107a1cc5b9b513de2ebe Mon Sep 17 00:00:00 2001 From: wuqingwei Date: Mon, 26 Oct 2026 21:12:10 +0800 Subject: [PATCH 2/4] feat(linux): build native addon on Linux and open first window Move five macOS-only napi methods to a macOS-only impl block on Linux (napi-derive registers cfg'd methods from the main impl even where their callbacks do not exist), wire the move patch into build.ts on Linux only, and record the verified Xvfb first window in LINUX-PORT.md. --- .changeset/linux-port-initial.md | 2 +- LINUX-PORT.md | 55 ++++++-- apps/gpuix/native/build.ts | 7 ++ apps/gpuix/native/config.ts | 10 ++ .../native/macos-napi-methods-linux.patch | 119 ++++++++++++++++++ 5 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 apps/gpuix/native/macos-napi-methods-linux.patch diff --git a/.changeset/linux-port-initial.md b/.changeset/linux-port-initial.md index 44573e24..be88116e 100644 --- a/.changeset/linux-port-initial.md +++ b/.changeset/linux-port-initial.md @@ -2,4 +2,4 @@ "@1git/gpuix": minor --- -Add an initial Linux port: build the patched GPUIX addon on Linux, open browsers and files with xdg-open, copy with wl-copy/xclip/xsel, prompt SSH host trust and passwords with zenity, and package a self-contained Linux tarball with `bun run package:linux`. macOS behaviour is unchanged. +Add an initial Linux port: build the patched GPUIX addon on Linux, open browsers and files with xdg-open, copy with wl-copy/xclip/xsel, prompt SSH host trust and passwords with zenity, and package a self-contained Linux tarball with `bun run package:linux`. Five macOS-only native methods are moved to a macOS-only impl block on Linux so the addon compiles there; on Linux those features fall back through the existing renderer guards. macOS behaviour is unchanged. diff --git a/LINUX-PORT.md b/LINUX-PORT.md index 9ef88f37..3d5930aa 100644 --- a/LINUX-PORT.md +++ b/LINUX-PORT.md @@ -1,9 +1,10 @@ # Linux port (work in progress) -Status: scaffolding landed on branch `linux-port`, commit `feat(linux)`. -This is **not** a finished, runnable Linux app yet. The TypeScript side -builds and type-checks on Linux; the native GUI build has not been run -to a window yet (see "Not yet verified"). +Status: first Linux window achieved on branch `linux-port`. +The native addon builds on Linux and `bun run dev:app` opens a real +1git window (see "Verified"). This is still not a finished product: +the window was verified under Xvfb on Ubuntu 24.04, not yet on a real +GNOME X11/Wayland desktop, and the TODO list below still stands. ## Why this port is plausible @@ -62,6 +63,18 @@ The blockers were 1git's own macOS gates, not the engine: Editor catalog discovery is still macOS-only (see TODO). - `src/window-options.ts`: no blurred background or traffic lights on Linux; the compositor's native decorations are used. +- `native/macos-napi-methods-linux.patch` (Linux only, applied last): + moves five macOS-only `#[napi]` methods (`get_font_families`, + `get_system_appearance`, `set_window_appearance`, `set_close_blocked`, + `take_close_requested`) out of the main `GpuixRenderer` impl into a + macOS-only impl block. napi-derive registers `cfg`'d methods from + the main impl even on Linux, where their callbacks do not exist, + so the addon failed to compile without this move (five `E0425` + errors). A `cfg_attr` workaround was tried first and did not work. + On Linux those five methods are absent, and the TS callers already + guard them with `"method" in renderer` (system appearance falls back + to light, close protection and font list are unavailable, window + appearance is skipped). macOS keeps the original patch stack. - Changeset: `.changeset/linux-port-initial.md` (`@1git/gpuix`, minor). ## Ubuntu 20.04: the floor, and the build rule @@ -94,7 +107,7 @@ Runtime dependencies for the tarball (to install, not bundle): (X11), plus the desktop's Wayland/X11 libraries and a Vulkan driver (or Mesa lavapipe for software rendering). -## Verified in this session (Ubuntu 24.04, no display, no Rust) +## Verified in this session (Ubuntu 24.04, Xvfb, Rust 1.97.1) - `bun install --frozen-lockfile`: ok (616 packages). - `bun run build` (turbo, all 5 tasks): ok. @@ -104,15 +117,35 @@ Runtime dependencies for the tarball (to install, not bundle): clipboard on a machine with no wl-copy/xclip/xsel now fails with the intended "install wl-clipboard or xclip" message instead of a raw spawn error. +- **Native build:** `bun run --cwd apps/gpuix build:native` succeeds + and writes a 51 MB `apps/gpuix/.cache/native/gpuix-native.node`. + System deps needed: Rust 1.97.1 (rustup, pinned by GPUIX), + `libx11-xcb-dev`, `libxkbcommon-x11-dev`, `libxcb*-dev`, + `libwayland-dev`, `libfontconfig-dev`, `libvulkan1`, + `mesa-vulkan-drivers`. Two environment traps, both hit in this + session and worth knowing: (a) this VM's egress proxy credential + rotates on every new exec session, so a `cargo fetch` that shares + its session with later commands stalls on downloads; fetch once in + a single session, then build with `CARGO_NET_OFFLINE=true`. + (b) VM whole-machine replacements wipe the apt layer (this session + lost `mesa-vulkan-drivers` and the `-dev` symlinks mid-build, which + surfaced as `rust-lld: unable to find library -lxkbcommon-x11` and + as a Vulkan surface failure); reinstall the deps after a + replacement, before rebuilding. +- **First window:** `DISPLAY=:99 bun --preload ./native/preload.ts + --hot src/main.tsx` under Xvfb (1920x1080) opens a 1280x820 window + titled "1git" with the real onboarding flow: "Welcome to 1git", + Light/Dark/Auto cards, step 1 of 4, "Skip setup" / "Continue". + Rendering uses llvmpipe (Mesa software Vulkan, no GPU in this VM). + Non-fatal log line under Xvfb: `Found no xinput mouse pointers` + (Xvfb has no input devices; a real desktop has them). + Screenshot: `~/workspace/your_files/1git-linux-first-window.png`. ## Not yet verified / TODO -1. **Native build and first window.** Install Rust (GPUIX pins - 1.97.1 in its `rust-toolchain.toml`) and the GPUI Linux system - libraries, then run `bun run --cwd apps/gpuix build:native` and - `bun run dev:app` under Xvfb and then on a real GNOME X11 and - Wayland session. Expect Vulkan/fontconfig issues to surface here - first. This is the main remaining risk. +1. **Real desktop run.** The window was verified under Xvfb only. + Next: run on a real GNOME X11 session and a Wayland session, + including mouse/keyboard input, window resize, and HiDPI. 2. **Cmd -> Ctrl sweep.** Many handlers already accept `cmd || ctrl`, but pure `cmd` checks (quick switcher, Cmd-click, settings shortcuts) will not fire on Linux, where GPUI reports the Super key diff --git a/apps/gpuix/native/build.ts b/apps/gpuix/native/build.ts index e0afacdf..7896e5c4 100644 --- a/apps/gpuix/native/build.ts +++ b/apps/gpuix/native/build.ts @@ -53,6 +53,7 @@ import { menuPlacementPlatformPatchPath, systemAppearancePatchPath, systemAppearancePlatformPatchPath, + macosNapiMethodsPatchPath, } from "./config"; const supportedNativeBuild = @@ -159,6 +160,12 @@ if ( diffTrailingContextPatchPath, scrollbarVisibilityPatchPath, fontFamiliesPatchPath, + // Linux only, applied last: moves five macOS-only #[napi] methods + // out of the main GpuixRenderer impl. napi-derive registers cfg'd + // methods from the main impl even where their callbacks do not + // exist, so without this the Linux addon does not compile. + // macOS keeps the original patch stack unchanged. + ...(process.platform === "linux" ? [macosNapiMethodsPatchPath] : []), ]; // Later patches can change an earlier patch's context. Unwind known patches diff --git a/apps/gpuix/native/config.ts b/apps/gpuix/native/config.ts index c9b83846..272384fe 100644 --- a/apps/gpuix/native/config.ts +++ b/apps/gpuix/native/config.ts @@ -167,6 +167,15 @@ export const fontFamiliesPatchPath = resolve( import.meta.dir, "font-families.patch" ); +// Linux only: moves five macOS-only #[napi] methods out of the main +// GpuixRenderer impl into a macOS-only impl block. napi-derive registers +// cfg'd methods from the main impl even on Linux, where their callbacks +// do not exist, so the Linux addon fails to compile without this move. +// TS callers already guard these methods with `"method" in renderer`. +export const macosNapiMethodsPatchPath = resolve( + import.meta.dir, + "macos-napi-methods-linux.patch" +); export const nativeBuildId = createHash("sha256") .update(nativeRevision) .update(readFileSync(patchPath)) @@ -213,5 +222,6 @@ export const nativeBuildId = createHash("sha256") .update(readFileSync(diffTrailingContextPatchPath)) .update(readFileSync(scrollbarVisibilityPatchPath)) .update(readFileSync(fontFamiliesPatchPath)) + .update(readFileSync(macosNapiMethodsPatchPath)) .update(`${process.platform}-${process.arch}-release-v1`) .digest("hex"); diff --git a/apps/gpuix/native/macos-napi-methods-linux.patch b/apps/gpuix/native/macos-napi-methods-linux.patch new file mode 100644 index 00000000..22ff5dda --- /dev/null +++ b/apps/gpuix/native/macos-napi-methods-linux.patch @@ -0,0 +1,119 @@ +--- a/packages/native/src/renderer.rs ++++ b/packages/native/src/renderer.rs +@@ -1191,19 +1191,7 @@ + } + } + +- /// Synchronous native veto; set before starting a protected operation. +- #[cfg(target_os = "macos")] +- #[napi] +- pub fn set_close_blocked(&self, blocked: bool) { +- self.close_protection.0.lock().unwrap().0 = blocked; +- } + +- /// Consume a blocked close/quit attempt without queuing a later quit. +- #[cfg(target_os = "macos")] +- #[napi] +- pub fn take_close_requested(&self) -> bool { +- std::mem::take(&mut self.close_protection.0.lock().unwrap().1) +- } + + /// Initialize GPUI using the native event-loop architecture for this OS. + #[napi] +@@ -1822,39 +1810,8 @@ + )) + } + +- /// Installed CoreText families, without opening a system font panel. +- #[cfg(target_os = "macos")] +- #[napi] +- pub fn get_font_families(&self) -> Vec { +- crate::installed_font_families() +- } + +- /// Read the OS preference, unaffected by an explicit per-window appearance. +- #[cfg(target_os = "macos")] +- #[napi] +- pub fn get_system_appearance(&self) -> String { +- match gpui_macos::system_appearance() { +- gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => "dark", +- _ => "light", +- }.to_string() +- } + +- /// Match this window's AppKit material and menus to its explicit app theme. +- #[cfg(target_os = "macos")] +- #[napi] +- pub fn set_window_appearance(&self, appearance: String) -> Result<()> { +- let dark = match appearance.as_str() { +- "light" => false, +- "dark" => true, +- _ => return Err(Error::from_reason("Window appearance must be light or dark")), +- }; +- update_window_without_view(|window, _cx| -> Result<()> { +- gpui_macos::set_window_appearance(window, dark) +- .map_err(|error| Error::from_reason(error.to_string()))?; +- window.refresh(); +- Ok(()) +- })? +- } + + #[napi] + pub fn set_window_title(&self, title: String) -> Result<()> { +@@ -7546,3 +7503,56 @@ + } + } + } ++ ++#[cfg(target_os = "macos")] ++#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] ++#[napi] ++impl GpuixRenderer { ++ /// Synchronous native veto; set before starting a protected operation. ++ #[cfg(target_os = "macos")] ++ #[napi] ++ pub fn set_close_blocked(&self, blocked: bool) { ++ self.close_protection.0.lock().unwrap().0 = blocked; ++ } ++ ++ /// Consume a blocked close/quit attempt without queuing a later quit. ++ #[cfg(target_os = "macos")] ++ #[napi] ++ pub fn take_close_requested(&self) -> bool { ++ std::mem::take(&mut self.close_protection.0.lock().unwrap().1) ++ } ++ ++ /// Installed CoreText families, without opening a system font panel. ++ #[cfg(target_os = "macos")] ++ #[napi] ++ pub fn get_font_families(&self) -> Vec { ++ crate::installed_font_families() ++ } ++ ++ /// Read the OS preference, unaffected by an explicit per-window appearance. ++ #[cfg(target_os = "macos")] ++ #[napi] ++ pub fn get_system_appearance(&self) -> String { ++ match gpui_macos::system_appearance() { ++ gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => "dark", ++ _ => "light", ++ }.to_string() ++ } ++ ++ /// Match this window's AppKit material and menus to its explicit app theme. ++ #[cfg(target_os = "macos")] ++ #[napi] ++ pub fn set_window_appearance(&self, appearance: String) -> Result<()> { ++ let dark = match appearance.as_str() { ++ "light" => false, ++ "dark" => true, ++ _ => return Err(Error::from_reason("Window appearance must be light or dark")), ++ }; ++ update_window_without_view(|window, _cx| -> Result<()> { ++ gpui_macos::set_window_appearance(window, dark) ++ .map_err(|error| Error::from_reason(error.to_string()))?; ++ window.refresh(); ++ Ok(()) ++ })? ++ } ++} From 5424212c96abcb8d7e925d8e97bd2acfe35dd350 Mon Sep 17 00:00:00 2001 From: wuqingwei Date: Mon, 26 Oct 2026 21:33:27 +0800 Subject: [PATCH 3/4] feat(linux): package Linux tarball with Linux-only remote helpers package-linux.ts previously built all remote helpers including darwin targets, which cannot cross-build from Linux. Ship only linux-* helpers in the Linux tarball and state the verified glibc 2.39 floor in the tarball README and LINUX-PORT.md. --- LINUX-PORT.md | 43 ++++++++++++++++++++++-------- apps/gpuix/native/package-linux.ts | 30 ++++++++++++++++----- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/LINUX-PORT.md b/LINUX-PORT.md index 3d5930aa..715c15ff 100644 --- a/LINUX-PORT.md +++ b/LINUX-PORT.md @@ -140,36 +140,57 @@ Runtime dependencies for the tarball (to install, not bundle): Non-fatal log line under Xvfb: `Found no xinput mouse pointers` (Xvfb has no input devices; a real desktop has them). Screenshot: `~/workspace/your_files/1git-linux-first-window.png`. +- **Packaged tarball (2026-10-26):** `bun run --cwd apps/gpuix + package:linux` produces `build/linux/1git-linux-x64.tar.gz` + (76 MB, sha256 `10a7ceb4…bfb`). Contents: `bin/1git` (Bun + compiled, 105 MB), `Frameworks/gpuix-native.node` (51 MB), + `Resources/app.js`, sharp Linux runtime, and Linux remote + helpers only. Two packaging fixes landed with it: + `package-linux.ts` now builds/copies only `linux-*` remote + helpers (the darwin helpers cannot be cross-built from Linux), + and the tarball README states the real glibc floor (see below). + The extracted copy was smoke-tested under Xvfb and opens the + same Welcome screen as the dev build. +- **glibc floor, verified by symbol scan:** `gpuix-native.node` + requires up to `GLIBC_2.39`, so this development tarball runs on + Ubuntu 24.04+ only, **not** on 20.04 (glibc 2.31), despite the + original 20.04 target. A 20.04 tarball must be built on 20.04 + itself; that is still pending and is the main blocker for the + stated minimum version. ## Not yet verified / TODO -1. **Real desktop run.** The window was verified under Xvfb only. - Next: run on a real GNOME X11 session and a Wayland session, - including mouse/keyboard input, window resize, and HiDPI. -2. **Cmd -> Ctrl sweep.** Many handlers already accept `cmd || ctrl`, +1. **Real desktop run.** The window was verified under Xvfb only + (dev build and packaged tarball alike). Next: run on a real + GNOME X11 session and a Wayland session, including + mouse/keyboard input, window resize, and HiDPI. +2. **Ubuntu 20.04 build.** Rebuild `build:native` and + `package:linux` on a 20.04 host/container and verify the + tarball's glibc ceiling is 2.31 before claiming 20.04 support. +3. **Cmd -> Ctrl sweep.** Many handlers already accept `cmd || ctrl`, but pure `cmd` checks (quick switcher, Cmd-click, settings shortcuts) will not fire on Linux, where GPUI reports the Super key as `cmd`. Map them to Ctrl on Linux, file by file, with tests. -3. **Editor catalog on Linux.** Discover `code`, `cursor`, `zed`, +4. **Editor catalog on Linux.** Discover `code`, `cursor`, `zed`, `subl` from PATH and `.desktop` files; the current Linux branch only handles an explicit executable path or the default handler. -4. **File-menu labels and icons.** "Reveal in Finder" and the +5. **File-menu labels and icons.** "Reveal in Finder" and the Finder.app icon path are still macOS strings; rename to the file manager on Linux and drop the Finder icon lookup. -5. **Credentials.** Account secrets go through GPUI's +6. **Credentials.** Account secrets go through GPUI's `read/write_credentials` (added by `accounts.patch`). Verify it against GNOME Keyring and KWallet on Linux, and decide the fallback for headless/no-keyring machines. Do not silently fall back to plaintext. -6. **Updater.** Sparkle is macOS-only. Either ship Linux with no +7. **Updater.** Sparkle is macOS-only. Either ship Linux with no auto-updater (v1, documented in the tarball README) or adopt GPUIX's GitHub-Releases `checkUpdate` flow. -7. **Packaging.** The tarball is a stopgap. A `.deb` (or AppImage via +8. **Packaging.** The tarball is a stopgap. A `.deb` (or AppImage via `cargo-packager`, as GPUIX upstream does) with a real `.desktop` file and icon is the proper distribution format. -8. **Tests.** The vibrancy, traffic-light, signing, notarization and +9. **Tests.** The vibrancy, traffic-light, signing, notarization and macOS launch integration suites are macOS-only; they need Linux equivalents or explicit skips, not silent passes. -9. **Push decision.** This branch lives only in this workspace. +10. **Push decision.** This branch lives only in this workspace. Nothing has been pushed to `1bye/1git` and no PR has been opened; pushing upstream or forking is a separate decision for the user. diff --git a/apps/gpuix/native/package-linux.ts b/apps/gpuix/native/package-linux.ts index 96a8b6b4..e0f0c181 100644 --- a/apps/gpuix/native/package-linux.ts +++ b/apps/gpuix/native/package-linux.ts @@ -110,10 +110,23 @@ try { resolve(resources, "RepositoryPatterns.LICENSE") ); await copyImageRuntimeLinux(resources, arch); - await run([process.execPath, "run", "native/build-remote-helpers.ts"]); + // Linux tarball ships Linux remote helpers only: the darwin helpers + // cannot be cross-built from Linux (no Apple SDK/linker here). A + // Linux client can therefore deploy helpers to Linux remotes only + // in this build; macOS remotes need the macOS build's helpers. + const linuxHelperTargets = remoteHelperTargets.filter((target) => + target.startsWith("linux-") + ); + + await run([ + process.execPath, + "run", + "native/build-remote-helpers.ts", + ...linuxHelperTargets, + ]); await mkdir(resolve(resources, "remote-helpers")); - for (const target of remoteHelperTargets) { + for (const target of linuxHelperTargets) { for (const extension of ["gz", "json"]) { await copyFile( resolve(appDirectory, `build/remote-helpers/${target}.${extension}`), @@ -138,10 +151,15 @@ Terminal=false resolve(root, "README.txt"), `1git ${version} - Linux beta -Requirements: Ubuntu 20.04 or later (glibc 2.31+), x86_64 or ARM64, -a Wayland or X11 session, Git, and xdg-utils. For SSH host prompts, -install zenity. For clipboard, install wl-clipboard (Wayland) or -xclip (X11). Bun, Rust and Xcode are not needed to run this app. +Requirements: a Wayland or X11 session, Git, and xdg-utils. For SSH +host prompts, install zenity. For clipboard, install wl-clipboard +(Wayland) or xclip (X11). Bun, Rust and Xcode are not needed to +run this app. + +glibc floor: this development build was made on Ubuntu 24.04 and +needs glibc 2.39 or newer, so it runs on Ubuntu 24.04+ but NOT on +20.04. A 20.04-compatible build must be built on Ubuntu 20.04 +itself (see LINUX-PORT.md); that is still pending. Install: 1. Extract this archive, e.g. to /opt/1git. From 0636f418949daa6a248c977691dbd189a458c811 Mon Sep 17 00:00:00 2001 From: wuqingwei Date: Mon, 26 Oct 2026 22:00:03 +0800 Subject: [PATCH 4/4] docs(linux): record real-machine verification and fork push --- LINUX-PORT.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/LINUX-PORT.md b/LINUX-PORT.md index 715c15ff..8e83dbb0 100644 --- a/LINUX-PORT.md +++ b/LINUX-PORT.md @@ -160,10 +160,12 @@ Runtime dependencies for the tarball (to install, not bundle): ## Not yet verified / TODO -1. **Real desktop run.** The window was verified under Xvfb only - (dev build and packaged tarball alike). Next: run on a real - GNOME X11 session and a Wayland session, including - mouse/keyboard input, window resize, and HiDPI. +1. **Real desktop run.** Xvfb verification done (dev build and + packaged tarball alike). User-verified 2026-10-26 on a real + Ubuntu 24.04 machine with the packaged tarball: window opens, + mouse input normal, window resize works. Still untested on a + real machine: Wayland vs X11 session type (not recorded), + keyboard shortcuts (see Cmd -> Ctrl below), and HiDPI. 2. **Ubuntu 20.04 build.** Rebuild `build:native` and `package:linux` on a 20.04 host/container and verify the tarball's glibc ceiling is 2.31 before claiming 20.04 support. @@ -191,6 +193,8 @@ Runtime dependencies for the tarball (to install, not bundle): 9. **Tests.** The vibrancy, traffic-light, signing, notarization and macOS launch integration suites are macOS-only; they need Linux equivalents or explicit skips, not silent passes. -10. **Push decision.** This branch lives only in this workspace. - Nothing has been pushed to `1bye/1git` and no PR has been opened; - pushing upstream or forking is a separate decision for the user. +10. **Push status.** Pushed 2026-10-26 to the user's fork + `wuqingwei/1git`, branch `linux-port` (3 commits: scaffolding, + native build + first window, Linux tarball packaging). Upstream + `origin` (`1bye/1git`) was not touched and no PR has been opened. + Remote `wuqingwei` in this worktree points at the fork.