Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@
// Knip loads its preprocessor through a CLI option; native verification and
// worktree setup (from t3.json) run directly. release-desktop.yml bundles the
// Windows verify script with esbuild through a path knip does not trace,
// electron-builder loads the macOS signer by path, and mobile-e2e.ts runs
// electron-builder loads the macOS signer and the Linux afterPack hook by path, and mobile-e2e.ts runs
// agent-device's CLI by path, and mobile-behavior.ts spawns its runner by path.
// release-desktop.yml runs the desktop launch check through xvfb-run, which knip
// does not trace.
"entry": [
// These fixture scripts are injected into a browser page by path.
"fixtures/*/*.js",
"knip-schemas.ts",
"linux-after-pack.ts",
"mobile-behavior/run.ts",
"mobile-native-client.ts",
"setup-worktree.ts",
Expand Down
1 change: 1 addition & 0 deletions scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2890,6 +2890,7 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
buildConfig.toolsets = { appimage: "1.0.3" };
const path = yield* Path.Path;
const repoRoot = yield* RepoRoot;
buildConfig.afterPack = path.join(repoRoot, "scripts/linux-after-pack.ts");
buildConfig.linux = {
target: target === "AppImage" ? [target, "deb"] : [target],
executableName: "t3code",
Expand Down
43 changes: 43 additions & 0 deletions scripts/linux-after-pack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @effect-diagnostics nodeBuiltinImport:off - Applies real file modes in a temporary app folder.
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as NodeFSP from "node:fs/promises";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import { describe, expect, it } from "vite-plus/test";

import { removeSharedWriteAccess } from "./linux-after-pack.ts";

describe.skipIf(HostProcess.Platform.defaultValue() === "win32")("removeSharedWriteAccess", () => {
it("removes group and other write access from the packed app only", async () => {
const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-after-pack-"));
try {
const appOutDir = NodePath.join(root, "app");
const modes = {
"app/t3code": [0o777, 0o755],
"app/resources.pak": [0o666, 0o644],
"app/chrome-sandbox": [0o4777, 0o4755],
"app/resources/app.asar": [0o644, 0o644],
"outside/file": [0o666, 0o666],
} as const;
await NodeFSP.mkdir(NodePath.join(appOutDir, "resources"), { recursive: true });
await NodeFSP.mkdir(NodePath.join(root, "outside"));
for (const [file, [before]] of Object.entries(modes)) {
await NodeFSP.writeFile(NodePath.join(root, file), "");
await NodeFSP.chmod(NodePath.join(root, file), before);
}
await NodeFSP.chmod(NodePath.join(appOutDir, "resources"), 0o777);
await NodeFSP.symlink("../outside", NodePath.join(appOutDir, "linked-dir"));

await removeSharedWriteAccess(appOutDir);

for (const [file, [, after]] of Object.entries(modes)) {
const { mode } = await NodeFSP.stat(NodePath.join(root, file));
expect([file, mode & 0o7777]).toEqual([file, after]);
}
const { mode: resourcesMode } = await NodeFSP.stat(NodePath.join(appOutDir, "resources"));
expect(resourcesMode & 0o7777).toBe(0o755);
} finally {
await NodeFSP.rm(root, { recursive: true, force: true });
}
});
});
29 changes: 29 additions & 0 deletions scripts/linux-after-pack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFSP from "node:fs/promises";
import * as NodeModule from "node:module";
import * as NodePath from "node:path";

const prepareServerCompileCache: (context: AfterPackContext) => Promise<void> =
NodeModule.createRequire(import.meta.url)("./prepare-server-compile-cache.cjs");

interface AfterPackContext {
readonly appOutDir: string;
}

// Electron's Linux zips can ship world-writable files (44.10.0 has 0777 binaries), and
// electron-builder copies zip modes verbatim into the .deb and AppImage.
export default async function afterPack(context: AfterPackContext): Promise<void> {
await prepareServerCompileCache(context);
await removeSharedWriteAccess(context.appOutDir);
}

// Walks by hand: recursive readdir follows directory symlinks out of the app folder.
export async function removeSharedWriteAccess(path: string): Promise<void> {
const stats = await NodeFSP.lstat(path);
if (stats.isSymbolicLink()) return;
if (stats.mode & 0o022) await NodeFSP.chmod(path, stats.mode & 0o7755);
if (!stats.isDirectory()) return;
for (const entry of await NodeFSP.readdir(path)) {
await removeSharedWriteAccess(NodePath.join(path, entry));
}
}
Loading