diff --git a/CHANGELOG.md b/CHANGELOG.md index 8398677..60e8bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased +- `apple build --path-free`: a checkout in a shared build folder outside the home folder is no longer a leak needle (dsymutil and the module cache record it; only home and user-name paths count). + ## 0.8.54 ยท 2026-10-26 - `apple build` is hermetic and can be path-free. `--derived-data ` (`apple.build.derivedData`) is xcodebuild `-derivedDataPath`; with `--out` (or `apple.build.out`) it defaults to `/DerivedData` instead of `~/Library/Developer/Xcode/DerivedData` (without `--out` Xcode's folder stays). `--path-free` (`apple.build.pathFree`) remaps the home folder, checkout, out and DerivedData out of the app binary and the archive's dSYMs / Symbols (Swift `-debug-prefix-map` / `-file-prefix-map`, clang `-ffile-prefix-map`; dwarf-with-dsym stays, symbol upload keeps working), scans the archive and stops before the export with the hit count and the first file + offsets (never the paths). `--scan-paths` only scans and warns. A `--out` outside the home folder prints a hint. `--json` / `latest.json` carry `derivedData`, `pathFree` and `leaks`. Store-pitfalls 124. diff --git a/cli/lib/path-free.ts b/cli/lib/path-free.ts index 4587520..21b5f95 100644 --- a/cli/lib/path-free.ts +++ b/cli/lib/path-free.ts @@ -52,7 +52,11 @@ export interface PathHit { file: string; kind: string; offset: number } * checkout. DerivedData is no needle: outside the home folder it is not the user's (dsymutil and Xcode record it in the * dSYM and the __swift_ast section and no prefix map reaches it); inside the home folder the home needle finds it. */ export function leakNeedles(i: { home: string; user: string; root: string }): { kind: string; value: string }[] { - const list = [{ kind: 'home folder', value: trim(i.home) + '/' }, { kind: 'user folder', value: `/Users/${i.user}/` }, { kind: 'checkout', value: trim(i.root) + '/' }]; + // the checkout is a needle only when it says something about the person: inside the home folder or naming the user. + // A shared build folder (/Users/Shared/..., /opt/...) recorded by dsymutil and Xcode's module cache is not private + const root = trim(i.root) + '/'; + const personal = root.startsWith(trim(i.home) + '/') || (i.user.length >= 3 && root.includes(`/${i.user}/`)); + const list = [{ kind: 'home folder', value: trim(i.home) + '/' }, { kind: 'user folder', value: `/Users/${i.user}/` }, ...(personal ? [{ kind: 'checkout', value: root }] : [])]; const seen = new Set(); return list.filter(n => n.value.length >= 6 && !seen.has(n.value) && seen.add(n.value)); } diff --git a/tests/path-free.test.ts b/tests/path-free.test.ts index 8278817..0a2e628 100644 --- a/tests/path-free.test.ts +++ b/tests/path-free.test.ts @@ -74,6 +74,10 @@ describe('leak scan', () => { test('needles are named by kind; /Users// is dropped when it equals the home folder', () => { expect(needles.map(n => n.kind)).toEqual(['home folder', 'checkout']); }); + test('a shared build folder outside the home folder is not a needle (dsymutil / module cache paths there are fine)', () => { + expect(leakNeedles({ home: '/Users/you', user: 'you', root: '/Users/Shared/ship/app/abc' }).map(n => n.kind)).toEqual(['home folder']); + expect(leakNeedles({ home: '/Users/you', user: 'you', root: '/opt/builds/you/app' }).map(n => n.kind)).toEqual(['home folder', 'checkout']); + }); test('counts hits, keeps first offsets per file, scans app, dSYMs and Symbols', () => { const r = scanForPaths(archiveScanDirs(arch), needles); expect(r.files).toBe(2);