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
28 changes: 14 additions & 14 deletions scripts/candidate-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ export function readFlatFiles(dir) {
return result;
}
function find(dir, name) {
const hits=[];
function visit(path,depth) {
if(depth>8) throw Error('Bundle directory depth exceeded');
for(const entry of readdirSync(path,{withFileTypes:true})) {
if(entry.isSymbolicLink()) continue; // app symlinks are not release assets.
const next=join(path,entry.name);
if(entry.isDirectory()) { if(!entry.name.endsWith('.app')) visit(next,depth+1); }
else if(entry.isFile() && entry.name===name) hits.push(next);
}
// Tauri puts finished files directly in each bundle-kind directory. Walking
// its sibling AppDir/deb staging trees can exceed bounds and select copies.
let path='.';
for(const segment of dir.split('/')) {
path=join(path,segment);const stat=lstatSync(path);
if(!stat.isDirectory() || stat.isSymbolicLink()) throw Error('Expected a regular bundle directory');
}
visit(dir,0); if(hits.length!==1) throw Error(`Expected one bundle ${name}, found ${hits.length}`); return hits[0];
const hits=readdirSync(dir,{withFileTypes:true}).filter(entry=>entry.name.toLowerCase()===name.toLowerCase());
if(hits.length!==1 || hits[0].name!==name) throw Error(`Expected one exact bundle ${name}, found ${hits.length}`);
if(!hits[0].isFile() || hits[0].isSymbolicLink()) throw Error('Expected a regular bundle');
return join(dir,name);
}
export function signFile(path, cliPath=resolve('ui-desktop/node_modules/@tauri-apps/cli/tauri.js')) {
try { execFileSync(process.execPath,[cliPath,'signer','sign',resolve(path)],{stdio:'pipe',timeout:120000,maxBuffer:1024*1024}); }
Expand All @@ -35,15 +35,15 @@ export function signFile(path, cliPath=resolve('ui-desktop/node_modules/@tauri-a
export function collect(platform, version, output='candidate-part', operations={signFile}) {
const names=bundleNames(version), root='ui-desktop/src-tauri/target';
const sources={
windows:[[`${root}/release/bundle`,names[0],names[0]]],
macos:[[`${root}/universal-apple-darwin/release/bundle`,names[1],names[1]],[`${root}/universal-apple-darwin/release/bundle`,'Tenebra.app.tar.gz',names[2]]],
linux:[[`${root}/release/bundle`,names[3],names[3]],[`${root}/release/bundle`,names[4],names[4]]],
windows:[[`${root}/release/bundle/nsis`,names[0],names[0]]],
macos:[[`${root}/universal-apple-darwin/release/bundle/dmg`,names[1],names[1]],[`${root}/universal-apple-darwin/release/bundle/macos`,'Tenebra.app.tar.gz',names[2]]],
linux:[[`${root}/release/bundle/deb`,names[3],names[3]],[`${root}/release/bundle/appimage`,names[4],names[4]]],
arch:[['packaging/arch',names[5],names[5]]],
};
if(!Object.hasOwn(sources,platform)) throw Error('Unsupported platform');
mkdirSync(output); // refuse reuse: same-name uploads must never clobber.
for(const [dir,raw,name] of sources[platform]) {
const source=platform==='arch'?join(dir,raw):find(dir,raw);
const source=find(dir,raw);
const stat=lstatSync(source);if(!stat.isFile()||stat.isSymbolicLink())throw Error('Expected a regular bundle');
copyFileSync(source,join(output,name));
// Sign the staged filenames, including deb/DMG/Arch. No key in argv/logs.
Expand Down
70 changes: 68 additions & 2 deletions scripts/candidate-files.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,75 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, rmdirSync } from 'node:fs';
import { join } from 'node:path';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, rmdirSync, symlinkSync, existsSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { collect, readFlatFiles } from './candidate-files.mjs';
const release='ui-desktop/src-tauri/target/release/bundle';
const universal='ui-desktop/src-tauri/target/universal-apple-darwin/release/bundle';
const layouts={
windows:[[`${release}/nsis/Tenebra_0.6.0_x64-setup.exe`,'Tenebra_0.6.0_x64-setup.exe']],
linux:[[`${release}/deb/Tenebra_0.6.0_amd64.deb`,'Tenebra_0.6.0_amd64.deb'],[`${release}/appimage/Tenebra_0.6.0_amd64.AppImage`,'Tenebra_0.6.0_amd64.AppImage']],
macos:[[`${universal}/dmg/Tenebra_0.6.0_universal.dmg`,'Tenebra_0.6.0_universal.dmg'],[`${universal}/macos/Tenebra.app.tar.gz`,'Tenebra_universal.app.tar.gz']],
};
function fixture(platform) {
for(const [path,name] of layouts[platform]) {mkdirSync(dirname(path),{recursive:true});writeFileSync(path,`bundle ${name}`);}
for(const id of platform==='macos'?['macos-arm64','macos-amd64']:[platform])writeFileSync(`core-buildinfo-${id}.json`,`report ${id}`);
}
const testSigner={signFile:path=>writeFileSync(path+'.sig','test signature')};
test('Linux collects finished kind outputs despite the actual deep AppDir and deb staging layout',()=>{
const dir=mkdtempSync(join(tmpdir(),'candidate-test-')),old=process.cwd();
try {
process.chdir(dir);fixture('linux');
for(const staging of ['appimage/Tenebra.AppDir','deb/Tenebra_0.6.0_amd64']) {
const path=`${release}/${staging}/usr/lib/a/b/c/d/e/f/g/h/i/j`;
mkdirSync(path,{recursive:true});writeFileSync(join(path,'cache'),'unrelated staging bytes');
}
collect('linux','0.6.0','output',testSigner);
for(const [,name] of layouts.linux)assert.equal(readFileSync(join('output',name),'utf8'),`bundle ${name}`);
assert.equal(readFlatFiles('output').size,5);
} finally {process.chdir(old);rmSync(dir,{recursive:true});}
});
for(const platform of ['windows','linux','macos']) {
test(`${platform} uses only exact final bundle-kind paths`,()=>{
const dir=mkdtempSync(join(tmpdir(),'candidate-test-')),old=process.cwd();
try {
process.chdir(dir);fixture(platform);collect(platform,'0.6.0','output',testSigner);
for(const [,name] of layouts[platform])assert.equal(readFileSync(join('output',name),'utf8'),`bundle ${name}`);
rmSync(layouts[platform][0][0]);
const fallback=join(dirname(dirname(layouts[platform][0][0])),'unexpected',layouts[platform][0][1]);
mkdirSync(dirname(fallback),{recursive:true});writeFileSync(fallback,'wrong-location bytes');
assert.throws(()=>collect(platform,'0.6.0','must-fail',testSigner));
} finally {process.chdir(old);rmSync(dir,{recursive:true});}
});
}
test('collection rejects a symlinked bundle-kind directory before reading or signing its target',()=>{
const dir=mkdtempSync(join(tmpdir(),'candidate-test-')),old=process.cwd();let signed=false;
try {
process.chdir(dir);fixture('windows');rmSync(`${release}/nsis`,{recursive:true});
mkdirSync('outside');writeFileSync('outside/Tenebra_0.6.0_x64-setup.exe','outside bytes');
symlinkSync(resolve('outside'),`${release}/nsis`,process.platform==='win32'?'junction':'dir');
assert.throws(()=>collect('windows','0.6.0','output',{signFile:()=>{signed=true;}}));assert.equal(signed,false);
} finally {process.chdir(old);rmSync(dir,{recursive:true});}
});
test('collection rejects a symbolic-link leaf instead of signing an alternate target',()=>{
const dir=mkdtempSync(join(tmpdir(),'candidate-test-')),old=process.cwd();let signed=false;
try {
process.chdir(dir);fixture('windows');rmSync(layouts.windows[0][0]);
// Junctions need no Windows symlink privilege and exercise lstat's link guard.
if(process.platform==='win32'){mkdirSync('outside');symlinkSync(resolve('outside'),layouts.windows[0][0],'junction');}
else{writeFileSync('outside','outside bytes');symlinkSync(resolve('outside'),layouts.windows[0][0]);}
assert.throws(()=>collect('windows','0.6.0','output',{signFile:()=>{signed=true;}}));assert.equal(signed,false);
} finally {process.chdir(old);rmSync(dir,{recursive:true});}
});
test('case-alias duplicate bundle names are refused on case-sensitive filesystems',t=>{
const dir=mkdtempSync(join(tmpdir(),'candidate-test-')),old=process.cwd();
try {
process.chdir(dir);writeFileSync('case-probe-A','probe');
if(existsSync('case-probe-a')){t.skip('Filesystem cannot contain distinct case-alias entries');return;}
fixture('linux');writeFileSync(`${release}/deb/tenebra_0.6.0_amd64.deb`,'ambiguous bytes');
assert.throws(()=>collect('linux','0.6.0','output',testSigner));
} finally {process.chdir(old);rmSync(dir,{recursive:true});}
});
test('Arch collection reads its one package without traversing dependency caches or rebuilt source',()=>{
const dir=mkdtempSync(join(tmpdir(),'candidate-test-')),old=process.cwd();
try {
Expand Down
Loading