Skip to content

Commit eaf2d80

Browse files
Merge pull request #55 from off-grid-ai/fix/asar-inventory-win-paths
fix(packaging): normalize asar entry separators so build-win passes
2 parents 7b2a4b4 + f7449f9 commit eaf2d80

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

scripts/lib/macos-artifact-integrity.d.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const ALLOWED_ASAR_ROOTS: readonly string[]
44
export const ALLOWED_ASAR_OUT_ROOTS: readonly string[]
55

66
export function verifyBundlePair(referenceBundle: string, candidateBundle: string): void
7+
export function assertAsarEntryInventory(entries: readonly string[]): void
78
export function assertAsarArchiveInventory(archive: string): void
89
export function assertAsarInventory(bundle: string): void
910
export function verifyDmgArtifact(dmgPath: string, referenceBundle: string): Promise<void>

scripts/lib/macos-artifact-integrity.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,15 @@ function isAllowedAsarOutEntry(entry) {
175175
return ALLOWED_ASAR_OUT_ROOTS.some((root) => entry === root || entry.startsWith(`${root}/`))
176176
}
177177

178-
export function assertAsarArchiveInventory(archive) {
179-
for (const entry of listPackage(archive)) {
178+
// Pure inventory check over an asar's entry list, split out so it unit-tests without
179+
// building a real archive. @electron/asar's listPackage returns entries with the OS
180+
// path separator — POSIX "/" on macOS/Linux but "\" on Windows (e.g. "\node_modules").
181+
// Normalize to "/" so the POSIX allowlist below holds on every build host; without it
182+
// EVERY entry read as an unexpected root on the Windows runner and this release gate
183+
// rejected an otherwise-valid app.asar (regressing the Windows build).
184+
export function assertAsarEntryInventory(entries) {
185+
for (const rawEntry of entries) {
186+
const entry = rawEntry.replace(/\\/g, '/')
180187
const segments = entry.split('/').filter(Boolean)
181188
const privateSegment = segments.find((segment) =>
182189
FORBIDDEN_PRIVATE_SEGMENTS.has(segment.toLowerCase())
@@ -198,6 +205,10 @@ export function assertAsarArchiveInventory(archive) {
198205
}
199206
}
200207

208+
export function assertAsarArchiveInventory(archive) {
209+
assertAsarEntryInventory(listPackage(archive))
210+
}
211+
201212
export function assertAsarInventory(bundle) {
202213
assertAsarArchiveInventory(path.join(bundle, 'Contents/Resources/app.asar'))
203214
}

src/main/__tests__/macos-artifact-integrity.integration.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ALLOWED_ASAR_ROOTS,
99
ALLOWED_ASAR_OUT_ROOTS,
1010
REQUIRED_MAC_BUNDLE_FILES,
11+
assertAsarEntryInventory,
1112
assertAsarInventory,
1213
verifyBundlePair,
1314
verifyZipArtifact
@@ -164,6 +165,52 @@ describe('macOS artifact integrity', () => {
164165
)
165166
})
166167

168+
// Regression: @electron/asar's listPackage yields OS-separator paths — backslashes on
169+
// the Windows runner ("\node_modules") — so the POSIX allowlist rejected EVERY entry
170+
// and failed an otherwise-valid app.asar (the build-win release regression). The pure
171+
// entry check must normalize separators so an identical tree passes on either host,
172+
// WITHOUT weakening the forbidden-path checks.
173+
describe('assertAsarEntryInventory — cross-platform path separators', () => {
174+
const POSIX_TREE = [
175+
'/package.json',
176+
'/out',
177+
'/out/main/index.js',
178+
'/out/preload/index.js',
179+
'/out/renderer/index.html',
180+
'/node_modules/electron/package.json'
181+
]
182+
const toWindows = (entries: string[]): string[] => entries.map((e) => e.replace(/\//g, '\\'))
183+
184+
it('accepts a valid runtime tree with POSIX separators', () => {
185+
expect(() => assertAsarEntryInventory(POSIX_TREE)).not.toThrow()
186+
})
187+
188+
it('accepts the SAME valid tree with Windows backslash separators', () => {
189+
expect(() => assertAsarEntryInventory(toWindows(POSIX_TREE))).not.toThrow()
190+
})
191+
192+
it('still rejects an unexpected root delivered with Windows separators', () => {
193+
expect(() => assertAsarEntryInventory(['\\marketing', '\\marketing\\list.csv'])).toThrow(
194+
'app.asar contains unexpected application root'
195+
)
196+
})
197+
198+
it('still rejects private state + build output with Windows separators', () => {
199+
expect(() => assertAsarEntryInventory(['\\out\\main\\.OFFGRID\\private.db'])).toThrow(
200+
'forbidden private state'
201+
)
202+
expect(() =>
203+
assertAsarEntryInventory(['\\out\\packaged-helpers-stale\\package\\x.js'])
204+
).toThrow('unexpected build output')
205+
})
206+
207+
it('still rejects a nested application bundle with Windows separators', () => {
208+
expect(() =>
209+
assertAsarEntryInventory(['\\out\\main\\Nested.APP\\Contents\\Info.plist'])
210+
).toThrow('nested application bundle')
211+
})
212+
})
213+
167214
it('enforces ASAR inventory on the real Windows artifact hook', async () => {
168215
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'offgrid-windows-artifact-'))
169216
tempRoots.push(root)

0 commit comments

Comments
 (0)