88 lines
6.0 KiB
JavaScript
88 lines
6.0 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import {execFile} from 'node:child_process';
|
|
import {promisify} from 'node:util';
|
|
import {fileURLToPath} from 'node:url';
|
|
|
|
const execute=promisify(execFile);
|
|
const FONTCONFIG='src/third_party/fontconfig/src';
|
|
function inside(root,target){const relative=path.relative(root,target);return relative===''||(!relative.startsWith(`..${path.sep}`)&&relative!=='..'&&!path.isAbsolute(relative));}
|
|
async function contained(root,target){
|
|
const resolved=await fs.realpath(target);
|
|
if(!inside(root,resolved))throw Error('path-outside-checkout');
|
|
return resolved;
|
|
}
|
|
async function nearestContained(root,target){
|
|
if(!inside(root,target))throw Error('path-outside-checkout');
|
|
let current=target;
|
|
while(true){try{return await contained(root,current);}catch(error){if(error.code!=='ENOENT')throw error;const parent=path.dirname(current);if(parent===current)throw error;current=parent;}}
|
|
}
|
|
async function git(repo,args){
|
|
const {stdout}=await execute('git',['--no-optional-locks','-C',repo,...args],{env:{...process.env,GIT_OPTIONAL_LOCKS:'0'},timeout:120000,maxBuffer:8*1024*1024,windowsHide:true});
|
|
return stdout.trim();
|
|
}
|
|
|
|
// Read-only prerequisite check. In particular, an existing directory is not
|
|
// proof of an independent checkout: Git silently searches ancestor repositories.
|
|
export async function nativePatchPreflight({checkoutRoot,configPath,excludeWindowsFontconfig=false,platform=process.platform}){
|
|
const result={scope:'Read-only patch target preflight; not patch import or build proof.',passed:false,required:[],excluded:[],errors:[]};
|
|
try{
|
|
const root=await fs.realpath(checkoutRoot);result.checkoutRoot=root;
|
|
const config=await contained(root,path.resolve(configPath));
|
|
const targets=JSON.parse(await fs.readFile(config,'utf8'));
|
|
if(!Array.isArray(targets)||targets.length===0||targets.length>64)throw Error('invalid-patch-config');
|
|
const seen=new Set();
|
|
for(const target of targets){
|
|
const row={repo:target?.repo,patchDir:target?.patch_dir,errors:[]};
|
|
try{
|
|
if(!target||typeof target.repo!=='string'||typeof target.patch_dir!=='string'||!target.repo||!target.patch_dir||path.isAbsolute(target.repo)||path.isAbsolute(target.patch_dir)||seen.has(target.repo))throw Error('invalid-patch-target');
|
|
seen.add(target.repo);
|
|
const repo=path.resolve(root,target.repo),patchDir=path.resolve(root,target.patch_dir);
|
|
await nearestContained(root,repo);
|
|
const patches=await contained(root,patchDir),list=await contained(root,path.join(patches,'.patches'));
|
|
// Electron's official reader keeps every list line, including blanks.
|
|
const text=await fs.readFile(list,'utf8');const names=text.split(/\r?\n/);if(names.at(-1)==='')names.pop();
|
|
if(!names.length||names.some(name=>!name||path.isAbsolute(name)))throw Error('invalid-patch-list');
|
|
for(const name of names){const file=await contained(root,path.resolve(patches,name));if(!inside(patches,file))throw Error('patch-outside-patch-directory');}
|
|
row.patchCount=names.length;
|
|
if(target.repo===FONTCONFIG&&excludeWindowsFontconfig){
|
|
if(platform!=='win32')throw Error('fontconfig-exclusion-requires-windows');
|
|
const deps=await fs.readFile(await contained(root,path.join(root,'src/DEPS')),'utf8');
|
|
if(!/['"]src\/third_party\/fontconfig\/src['"]\s*:\s*\{[^}]*['"]condition['"]\s*:\s*['"]checkout_linux['"]/s.test(deps))throw Error('fontconfig-exclusion-condition-unverified');
|
|
row.reason='Explicit Windows exclusion: Chromium DEPS checkout_linux';result.excluded.push(row);continue;
|
|
}
|
|
const own=await contained(root,repo);
|
|
try{await contained(root,path.join(own,'.git'));}catch(error){if(error.code==='ENOENT')throw Error('missing-own-git-marker',{cause:error});throw error;}
|
|
const top=await git(own,['rev-parse','--show-toplevel']);
|
|
if(await fs.realpath(top)!==own)throw Error('ancestor-git-root');
|
|
const gitDir=await git(own,['rev-parse','--absolute-git-dir']);await contained(root,gitDir);
|
|
const am=path.resolve(own,await git(own,['rev-parse','--git-path','rebase-apply']));
|
|
await nearestContained(root,am);
|
|
try{await fs.stat(am);throw Error('git-am-in-progress');}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
row.head=await git(own,['rev-parse','--verify','HEAD']);
|
|
row.trackedClean=(await git(own,['status','--porcelain=v1','--untracked-files=no']))==='';
|
|
if(!row.trackedClean)throw Error('tracked-worktree-not-clean');
|
|
}catch(error){row.errors.push(error.code==='ENOENT'?'missing-required-path':error.message||'preflight-failed');}
|
|
result.required.push(row);
|
|
}
|
|
result.errors=result.required.filter(row=>row.errors.length).map(row=>({repo:row.repo,errors:row.errors}));
|
|
result.passed=result.errors.length===0;
|
|
}catch(error){result.errors.push({errors:[error.code==='ENOENT'?'missing-required-path':error.message||'preflight-failed']});}
|
|
result.requiredCount=result.required.length;result.excludedCount=result.excluded.length;
|
|
return result;
|
|
}
|
|
|
|
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
|
|
const args=process.argv.slice(2);const allowed=new Set(['--checkout-root','--config','--exclude-windows-fontconfig']);let checkoutRoot,configPath,excludeWindowsFontconfig=false;
|
|
try{
|
|
for(let i=0;i<args.length;i++){
|
|
if(!allowed.has(args[i]))throw Error('invalid-cli-argument');
|
|
if(args[i]==='--exclude-windows-fontconfig'){excludeWindowsFontconfig=true;continue;}
|
|
const value=args[++i];if(!value||value.startsWith('--'))throw Error('missing-cli-value');
|
|
if(args[i-1]==='--checkout-root')checkoutRoot=value;else configPath=value;
|
|
}
|
|
if(!checkoutRoot||!configPath)throw Error('checkout-root-and-config-required');
|
|
const result=await nativePatchPreflight({checkoutRoot,configPath,excludeWindowsFontconfig});console.log(JSON.stringify(result,null,2));process.exitCode=result.passed?0:1;
|
|
}catch(error){console.log(JSON.stringify({passed:false,errors:[error.message]}));process.exitCode=1;}
|
|
}
|