From 078e98745ddd6f6f41d0e3247e6ee0abf08ea152 Mon Sep 17 00:00:00 2001 From: Joob1n Date: Sat, 22 Aug 2026 15:51:51 +0800 Subject: [PATCH 1/2] perf(desktop): drop Git Credential Manager from the packaged Git runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled Git distribution ships Git Credential Manager and the .NET runtime it needs. Maka cannot reach either: every git invocation sets `credential.helper=` explicitly (`git-workspace-service.ts:1257`, `:2873`), so the helper is not merely unused — it is switched off at the call site. Credentials live in Maka's own `credentials.json` under the permission contract in SECURITY.md. Measured on two macOS arm64 packages built from the same tree, one with this change and one without: | | unpruned | pruned | delta | | --- | ---: | ---: | ---: | | `Maka.app` | 602 MB | 481 MB | **−121 MB** | | bundled Git | 160 MB | 42 MB | −118 MB | | `libexec/git-core` files | 414 | 191 | −223 | The 223 files are 203 `.dll`, 16 `.dylib`, and 5 credential-manager entry points. The `.dll` files are `PE32+ Mono/.Net assembly` — Windows IL that macOS never loads. Excluded by name rather than by directory because the payload is interleaved with git's own commands inside one flat `libexec/git-core`. That makes over-matching the risk worth testing rather than assuming, so `verify-packaged-app` now asserts both halves: the commands Maka dispatches to as separate programs must be present, and nothing from the .NET runtime may be. Checking only the absence would pass just as well for an empty directory. Verified against both packages under the environment `isolatedGitEnvironment` actually builds — `GIT_EXEC_PATH`, `GIT_TEMPLATE_DIR`, `GIT_CONFIG_NOSYSTEM`, `credential.helper=` — running every subcommand `git-workspace-service.ts` invokes: init · config · add · commit · status · rev-parse cat-file · for-each-ref · worktree list · worktree add All pass on the pruned build with no warnings, and the unpruned build passes the same set, so the comparison isolates the change rather than a pre-existing failure. `git --version` reports 2.53.0 on both and `share/git-core/templates` is untouched. The assertion is scoped to POSIX. The Windows distribution has a different layout and its own `.dll` set that git itself loads, so this exclusion does not apply there; trimming Windows needs its own measurement first. Fixes #3428 Generated-by: Claude Opus 5 --- apps/desktop/electron-builder.config.mjs | 19 ++++++++++ scripts/verify-packaged-app.mjs | 47 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/apps/desktop/electron-builder.config.mjs b/apps/desktop/electron-builder.config.mjs index 03436ca5ff..ad7593f065 100644 --- a/apps/desktop/electron-builder.config.mjs +++ b/apps/desktop/electron-builder.config.mjs @@ -11,6 +11,24 @@ const { runtimeHostSetupPackage } = resolveProductManifestIdentity({ cliManifest: readManifest('../../packages/cli/package.json'), }); +// Git Credential Manager and the .NET runtime it needs are 223 files and +// ~103 MiB of the bundled Git distribution, and Maka never runs them: every +// git invocation sets `credential.helper=` explicitly +// (`git-workspace-service.ts:1257`, `:2873`), so the helper is not merely +// unused — it is switched off at the call site. Credentials live in Maka's +// own `credentials.json` under the SECURITY.md permission contract. +// +// Excluded by name because the payload is interleaved with git's own commands +// inside one flat `libexec/git-core` — there is no directory to drop. `.dll` +// and `.dylib` appear nowhere else in the distribution, and every one of the +// 16 dylibs is a .NET, Avalonia or Skia runtime library. +const GIT_CREDENTIAL_MANAGER_EXCLUDES = [ + '!libexec/git-core/*.dll', + '!libexec/git-core/*.dylib', + '!libexec/git-core/git-credential-manager*', + '!libexec/git-core/createdump', +]; + export default { appId: 'com.maka.desktop', productName: 'Maka', @@ -50,6 +68,7 @@ export default { { from: '../../node_modules/dugite/git', to: 'git', + filter: ['**/*', ...GIT_CREDENTIAL_MANAGER_EXCLUDES], }, { from: 'bundled-git.json', diff --git a/scripts/verify-packaged-app.mjs b/scripts/verify-packaged-app.mjs index ab4446efb7..e84601125b 100644 --- a/scripts/verify-packaged-app.mjs +++ b/scripts/verify-packaged-app.mjs @@ -705,6 +705,13 @@ export async function assertPackagedResources( requirePath, forbidPath = assertMissing, requireWindowsSandbox = process.platform === 'win32', + // The credential-manager exclusion is written against the POSIX Git + // distribution, whose commands sit in a flat `libexec/git-core`. The + // Windows distribution has a different layout (`git/cmd/git.exe`, and its + // own `.dll` set that git itself loads), so the exclusion does not apply + // there and neither does this assertion. Scoped rather than guessed: + // trimming Windows needs its own measurement first. + assertGitTree = process.platform !== 'win32', // The upgrade-lifecycle check runs this against a previously released // build, which predates the disclaimer being packaged. Requiring it there // would fail a release that was correct when it shipped. @@ -759,10 +766,50 @@ export async function assertPackagedResources( // app, and `distributionReady` is false for exactly this reason. join('bin', 'maka-cu'), join('tools', 'maka-cu'), + // Git Credential Manager and its .NET runtime are excluded from the + // packaged Git distribution: Maka sets `credential.helper=` on every git + // invocation, so nothing can reach them. Naming the entry point rather + // than the runtime keeps this readable; `assertPackagedGitIsComplete` + // covers the rest by listing the directory. + ...(assertGitTree ? [join('git', 'libexec', 'git-core', 'git-credential-manager')] : []), ]; for (const path of forbidden) { await forbidPath(join(resourcesPath, path)); } + if (assertGitTree) await assertPackagedGitIsComplete(resourcesPath, requirePath); +} + +/** + * The credential-manager exclusion is a name filter over one flat directory + * that also holds git's own commands, so it can over-match — and a git that + * lost `git-remote-http` fails at clone time in a user's hands, not here. + * + * Both halves are asserted: the commands Maka actually invokes must be + * present, and nothing from the .NET runtime may be. Checking only the + * absence would pass just as well for an empty directory. + */ +async function assertPackagedGitIsComplete(resourcesPath, requirePath) { + const gitCore = join(resourcesPath, 'git', 'libexec', 'git-core'); + // `git-workspace-service.ts` drives add / cat-file / commit / config / + // for-each-ref / init / rev-parse / status / worktree. Those are builtins + // reached through the `git` binary; what has to exist on disk is the + // binary itself plus the helpers git dispatches to as separate programs. + for (const name of ['git', 'git-remote-http', 'git-http-fetch', 'git-shell']) { + await requirePath(join(gitCore, name)); + } + await requirePath(join(resourcesPath, 'git', 'bin', 'git')); + await requirePath(join(resourcesPath, 'git', 'share', 'git-core', 'templates')); + + const entries = await readdir(gitCore, { withFileTypes: true }); + const runtimeLeftovers = entries + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .filter((name) => /\.(?:dll|dylib)$/u.test(name) || name.startsWith('git-credential-manager')); + if (runtimeLeftovers.length > 0) { + throw new Error( + `packaged git still carries the credential-manager runtime: ${runtimeLeftovers.join(', ')}`, + ); + } } /** From 074c9c1b9fb48a81067aa63d9a68fe26ff46d5f0 Mon Sep 17 00:00:00 2001 From: Joob1n Date: Sat, 22 Aug 2026 18:02:57 +0800 Subject: [PATCH 2/2] fix(desktop): prune the credential-manager runtime below the top level too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review found the exclusion and the verifier both scanned only the top of `libexec/git-core`, and checking a freshly built package confirms it: 13 localisation directories survived, each holding one `System.CommandLine.resources.dll`, along with GCM's own `NOTICE` and `uninstall.sh`. The verifier reported that tree as clean. A top-level-only scan is exactly the shape of check that passes while the thing it guards is still there, which is worse than no check — so both sides now recurse. `.so` joins the extension globs for the Linux distribution, where GCM is self-contained and its `libSkiaSharp.so` / `libHarfBuzzSharp.so` sit beside the binary. Neither was excluded nor detected before. Git's own commands in this directory are executables and shell scripts, never `.dll` / `.dylib` / `.so`, so the extension globs still cannot reach them. Measured on a rebuilt macOS arm64 package: | | before this fix | after | | --- | ---: | ---: | | `libexec/git-core` top-level files | 191 | 176 | | subdirectories | 14 | 1 (`mergetools`, git's own) | | `.dll` / `.dylib` / `.so`, recursive | 13 | **0** | | GCM `NOTICE` / `uninstall.sh` | 2 | 0 | `git`, `git-remote-http`, `git-http-fetch` and `git-shell` are still present. The remaining size delta is small — those resource assemblies are a megabyte between them — but a verifier that says "clean" has to mean it. Generated-by: Claude Opus 5 --- apps/desktop/electron-builder.config.mjs | 21 +++++++++-- scripts/verify-packaged-app.mjs | 45 ++++++++++++++++++++---- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/apps/desktop/electron-builder.config.mjs b/apps/desktop/electron-builder.config.mjs index ad7593f065..baeb09ad3d 100644 --- a/apps/desktop/electron-builder.config.mjs +++ b/apps/desktop/electron-builder.config.mjs @@ -22,11 +22,28 @@ const { runtimeHostSetupPackage } = resolveProductManifestIdentity({ // inside one flat `libexec/git-core` — there is no directory to drop. `.dll` // and `.dylib` appear nowhere else in the distribution, and every one of the // 16 dylibs is a .NET, Avalonia or Skia runtime library. +// `**/*` rather than `*`: the runtime is not flat. Alongside the assemblies at +// the top level, GCM ships 13 localisation directories each holding one +// `System.CommandLine.resources.dll`, and on Linux two native UI libraries +// (`libSkiaSharp.so`, `libHarfBuzzSharp.so`). A top-level-only glob leaves all +// of them behind — which it did, until a review caught it against a real +// package. +// +// `.so` is listed for the Linux distribution, where GCM is self-contained and +// its Skia/HarfBuzz libraries sit beside the binary. Git's own commands in +// this directory are executables and shell scripts, never `.dll`/`.dylib`/ +// `.so`, so the extension globs cannot reach them. const GIT_CREDENTIAL_MANAGER_EXCLUDES = [ - '!libexec/git-core/*.dll', - '!libexec/git-core/*.dylib', + '!libexec/git-core/**/*.dll', + '!libexec/git-core/**/*.dylib', + '!libexec/git-core/**/*.so', '!libexec/git-core/git-credential-manager*', '!libexec/git-core/createdump', + // GCM's own installation leftovers. `NOTICE` is its third-party attribution + // file, not git's — git's own notices live in `share/doc`, which this filter + // does not touch. + '!libexec/git-core/NOTICE', + '!libexec/git-core/uninstall.sh', ]; export default { diff --git a/scripts/verify-packaged-app.mjs b/scripts/verify-packaged-app.mjs index e84601125b..f1481a79c9 100644 --- a/scripts/verify-packaged-app.mjs +++ b/scripts/verify-packaged-app.mjs @@ -4,7 +4,7 @@ import { createReadStream, readFileSync } from 'node:fs'; import { access, mkdir, readFile, readdir } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { createServer } from 'node:net'; -import { join, resolve, sep } from 'node:path'; +import { join, relative, resolve, sep } from 'node:path'; import { ASSET_LICENSED_RENDERER_PACKAGES, collectProductionClosure, @@ -800,11 +800,12 @@ async function assertPackagedGitIsComplete(resourcesPath, requirePath) { await requirePath(join(resourcesPath, 'git', 'bin', 'git')); await requirePath(join(resourcesPath, 'git', 'share', 'git-core', 'templates')); - const entries = await readdir(gitCore, { withFileTypes: true }); - const runtimeLeftovers = entries - .filter((entry) => entry.isFile()) - .map((entry) => entry.name) - .filter((name) => /\.(?:dll|dylib)$/u.test(name) || name.startsWith('git-credential-manager')); + // Recursive, because the runtime is not flat: GCM ships 13 localisation + // directories each holding one `System.CommandLine.resources.dll`, and on + // Linux two native UI libraries beside the binary. A top-level-only scan + // reported a clean tree while all of that was still packaged — which is + // exactly what it did until a review checked a real artifact. + const runtimeLeftovers = await findCredentialManagerLeftovers(gitCore, gitCore); if (runtimeLeftovers.length > 0) { throw new Error( `packaged git still carries the credential-manager runtime: ${runtimeLeftovers.join(', ')}`, @@ -812,6 +813,38 @@ async function assertPackagedGitIsComplete(resourcesPath, requirePath) { } } +/** + * Every credential-manager artefact under one directory, as paths relative to + * the git-core root so the failure names where to look. + * + * Git's own commands here are executables and shell scripts, so matching on + * the .NET/native extensions cannot implicate them. `NOTICE` and + * `uninstall.sh` are GCM's installation leftovers, not git's — git keeps its + * notices in `share/doc`. + */ +async function findCredentialManagerLeftovers(directory, root) { + const leftovers = []; + const entries = await readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + const absolute = join(directory, entry.name); + if (entry.isDirectory()) { + leftovers.push(...(await findCredentialManagerLeftovers(absolute, root))); + continue; + } + if (!entry.isFile()) continue; + const isRuntimeLibrary = /\.(?:dll|dylib|so)$/u.test(entry.name); + const isCredentialManager = + entry.name.startsWith('git-credential-manager') || + entry.name === 'createdump' || + entry.name === 'NOTICE' || + entry.name === 'uninstall.sh'; + if (isRuntimeLibrary || isCredentialManager) { + leftovers.push(relative(root, absolute)); + } + } + return leftovers; +} + /** * Recursive content manifest of a directory tree: POSIX-normalized relative * paths, sorted, each with its file's SHA-256. Nothing is skipped — an install