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