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
36 changes: 36 additions & 0 deletions apps/desktop/electron-builder.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,41 @@ 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.
// `**/*` 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/**/*.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',
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] These globs only match the top level of libexec/git-core, and only macOS/Windows file types.

Two categories survive:

  • Localisation subdirectories. !libexec/git-core/*.dll does not match libexec/git-core/cs/System.CommandLine.resources.dll. All 13 GCM locale directories ship (~260 KiB total), together with NOTICE and uninstall.sh, which match no pattern at all.
  • Linux native libraries. There is no *.so entry. On the ubuntu-x64 distribution the GCM binary itself is caught by git-credential-manager*, but libSkiaSharp.so (~9.2 MiB) and libHarfBuzzSharp.so (~2.1 MiB) remain — roughly 11 MiB of the claimed saving is not actually removed there.

Also worth reconciling: the comment above says ".dll and .dylib appear nowhere else in the distribution", which holds for the flat directory but not for the locale subdirectories.

Adding '!libexec/git-core/*.so', '!libexec/git-core/*/' (or the explicit locale list), '!libexec/git-core/NOTICE' and '!libexec/git-core/uninstall.sh' closes both without changing the approach.


export default {
appId: 'com.maka.desktop',
productName: 'Maka',
Expand Down Expand Up @@ -50,6 +85,7 @@ export default {
{
from: '../../node_modules/dugite/git',
to: 'git',
filter: ['**/*', ...GIT_CREDENTIAL_MANAGER_EXCLUDES],
},
{
from: 'bundled-git.json',
Expand Down
82 changes: 81 additions & 1 deletion scripts/verify-packaged-app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -759,10 +766,83 @@ 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] "covers the rest by listing the directory" is not what the implementation does.

assertPackagedGitIsComplete filters with entry.isFile(), so directory entries are dropped before the name test ever runs — the locale subdirectories carrying System.CommandLine.resources.dll cannot be detected. The name test itself is also narrower than "the rest": NOTICE and uninstall.sh match neither branch.

The comment is the part most likely to mislead later: someone extending the exclusion list will reasonably trust that the verifier is a backstop for anything they miss, when it currently backstops only top-level .dll / .dylib / git-credential-manager*. Either narrow the wording to what is actually asserted, or widen the assertion to match the claim.

...(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'));

// 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(', ')}`,
);
}
}

/**
* 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;
}

/**
Expand Down