@@ -67,7 +67,9 @@ console.misc = function (...args: string[]) {
6767}
6868
6969let projectConfiguration : any ;
70- let buildFiles : { [ path : string ] : any } = [ ] ;
70+ let buildFiles : { [ path : string ] : any } = { } ;
71+ const CONFIG_CACHE_PATH = "./.atomtools-cache.json" ;
72+ const CACHE_VERSION = 1 ;
7173
7274function isIterable ( obj : any ) : boolean {
7375 // checks for null and undefined
@@ -111,6 +113,10 @@ async function populateToolPaths() {
111113 let success = true ;
112114 let promises = [ ]
113115 for ( let tool of Object . keys ( tools ) as ( string ) [ ] ) {
116+ if ( tools [ tool ] . path ) {
117+ console . debug ( ` -> Using cached ${ tool } at ${ tools [ tool ] . path } ` ) ;
118+ continue ;
119+ }
114120 promises . push (
115121 which ( tool ) . catch ( ( ) => null ) . then ( item => {
116122 if ( ! item ) {
@@ -147,6 +153,10 @@ async function populateDirectoryPaths() {
147153 let promises = [ ] ;
148154 for ( let dir of Object . keys ( directories ) as ( keyof typeof directories ) [ ] ) {
149155 let item = directories [ dir ] ;
156+ if ( item . path ) {
157+ console . debug ( ` -> Using cached ${ item . name } path at ${ item . path } ` ) ;
158+ continue ;
159+ }
150160 promises . push ( runCommand ( "sh" , [ "-c" , item . find [ 0 ] as string ] ) . catch ( ( ) => ( { code : - 1 , stdout : "" , stderr : "" } ) ) . then ( ( {
151161 stdout, stderr, code
152162 } ) => {
@@ -182,31 +192,61 @@ async function populateDirectoryPaths() {
182192let primaryTarget : string ;
183193
184194async function getBuildFiles ( ) {
185- // walk every subdirectory in project
186- for ( let item of projectConfiguration . source ) {
187- let folders = fs . readdirSync ( item )
188- for ( let folder of folders ) {
189- let fullPath = `${ item } /${ folder } ` ;
190- let stat = await promisify ( fs . stat ) ( fullPath ) ;
191- if ( stat . isDirectory ( ) ) {
192- folders . push ( ...fs . readdirSync ( fullPath ) . map ( subitem => `${ folder } /${ subitem } ` ) ) ;
195+ buildFiles = { } as any ;
196+ const importPromises : Promise < void > [ ] = [ ] ;
197+
198+ for ( let sourceRoot of projectConfiguration . source ) {
199+ const queue : string [ ] = [ sourceRoot ] ;
200+ while ( queue . length ) {
201+ const current = queue . pop ( ) as string ;
202+ let entries : string [ ] = [ ] ;
203+ try {
204+ entries = fs . readdirSync ( current ) ;
205+ } catch {
206+ continue ;
193207 }
194- if ( stat . isFile ( ) && ( folder . endsWith ( "/build.js" ) || folder . endsWith ( "/build.json" ) ) ) {
195- const file = await import ( fullPath ) ;
196- if ( ! file . default . atomtools ) continue ;
197- console . debug ( ` -> Found build file: ${ fullPath } ` ) ;
198- buildFiles [ fullPath ] = file . default ;
208+ for ( let entry of entries ) {
209+ const fullPath = `${ current } /${ entry } ` ;
210+ let stat ;
211+ try {
212+ stat = fs . statSync ( fullPath ) ;
213+ } catch {
214+ continue ;
215+ }
216+ if ( stat . isDirectory ( ) ) {
217+ queue . push ( fullPath ) ;
218+ continue ;
219+ }
220+ if ( entry === "build.js" || entry === "build.json" ) {
221+ importPromises . push (
222+ import ( fullPath ) . then ( file => {
223+ if ( ! file . default ?. atomtools ) return ;
224+ console . debug ( ` -> Found build file: ${ fullPath } ` ) ;
225+ buildFiles [ fullPath ] = file . default ;
226+ } ) . catch ( ( err ) => {
227+ console . warn ( ` Warning: skipping build file ${ fullPath } : ${ err } ` ) ;
228+ } )
229+ ) ;
230+ }
199231 }
200232 }
201233 }
234+
235+ await Promise . all ( importPromises ) ;
202236 console . info ( `-> Found ${ Object . keys ( buildFiles ) . length } build files` ) ;
203237}
204238
205239async function configure ( ) {
206- console . info ( "-> Configuring tools..." ) ;
207- await populateToolPaths ( ) ;
208- console . info ( "-> Getting required paths..." ) ;
209- await populateDirectoryPaths ( ) ;
240+ const cacheLoaded = await loadConfigurationCache ( ) ;
241+ if ( ! cacheLoaded ) {
242+ console . info ( "-> Configuring tools..." ) ;
243+ await populateToolPaths ( ) ;
244+ console . info ( "-> Getting required paths..." ) ;
245+ await populateDirectoryPaths ( ) ;
246+ await writeConfigurationCache ( ) ;
247+ } else {
248+ console . info ( "-> Using cached tool/path resolution..." ) ;
249+ }
210250 console . info ( "-> Enumerating build files..." ) ;
211251 await getBuildFiles ( ) ;
212252}
@@ -221,6 +261,65 @@ async function getCreationDateOfPath(path: string): Promise<number> {
221261 }
222262}
223263
264+ async function isOutputStale ( inputPath : string , outputPath : string ) : Promise < boolean > {
265+ // Skip work when the produced output is already newer than its input
266+ const [ inputTime , outputTime ] = await Promise . all ( [
267+ getCreationDateOfPath ( inputPath ) ,
268+ getCreationDateOfPath ( outputPath )
269+ ] ) ;
270+ return outputTime < inputTime ;
271+ }
272+
273+ async function loadConfigurationCache ( ) : Promise < boolean > {
274+ if ( ! fs . existsSync ( CONFIG_CACHE_PATH ) ) return false ;
275+ try {
276+ const cache = JSON . parse ( fs . readFileSync ( CONFIG_CACHE_PATH , "utf-8" ) ) ;
277+ if ( cache . version !== CACHE_VERSION ) return false ;
278+
279+ const configStamp = await getCreationDateOfPath ( "./build.config.js" ) ;
280+ if ( cache . configStamp !== configStamp ) return false ;
281+
282+ let toolsComplete = true ;
283+ for ( let tool of Object . keys ( projectConfiguration . tools || { } ) ) {
284+ const cachedPath = cache . tools ?. [ tool ] ;
285+ if ( ! cachedPath ) {
286+ toolsComplete = false ;
287+ continue ;
288+ }
289+ projectConfiguration . tools [ tool ] . path = cachedPath ;
290+ }
291+
292+ let pathsComplete = true ;
293+ for ( let pathKey of Object . keys ( projectConfiguration . paths || { } ) ) {
294+ const cachedPath = cache . paths ?. [ pathKey ] ;
295+ if ( ! cachedPath ) {
296+ pathsComplete = false ;
297+ continue ;
298+ }
299+ projectConfiguration . paths [ pathKey ] . path = cachedPath ;
300+ }
301+
302+ return toolsComplete && pathsComplete ;
303+ } catch {
304+ return false ;
305+ }
306+ }
307+
308+ async function writeConfigurationCache ( ) {
309+ const configStamp = await getCreationDateOfPath ( "./build.config.js" ) ;
310+ const cache = {
311+ version : CACHE_VERSION ,
312+ configStamp,
313+ tools : Object . fromEntries ( Object . entries ( projectConfiguration . tools || { } ) . map ( ( [ key , value ] : [ string , any ] ) => [ key , value . path ] ) ) ,
314+ paths : Object . fromEntries ( Object . entries ( projectConfiguration . paths || { } ) . map ( ( [ key , value ] : [ string , any ] ) => [ key , value . path ] ) )
315+ } ;
316+ try {
317+ fs . writeFileSync ( CONFIG_CACHE_PATH , JSON . stringify ( cache , null , 2 ) ) ;
318+ } catch ( err : any ) {
319+ console . warn ( ` Warning: Unable to write configuration cache: ${ err } ` ) ;
320+ }
321+ }
322+
224323async function buildTarget ( ) {
225324 console . log ( `-> Starting build for target "${ primaryTarget } "...` ) ;
226325 // Step 0: create the build directory
@@ -235,11 +334,16 @@ async function buildTarget() {
235334 exit ( 1 ) ;
236335 }
237336
337+ // reset per-run mutation markers
338+ target . wasModified = false ;
339+ const targetOutputPath = escapeCommand ( target . output , target . require || { } , "./" ) ;
340+
238341 // Step 1.5: see if the target is newer than its dependencies
239342 // if so, we can skip the build entirely
240- let targetTime = await getCreationDateOfPath ( escapeCommand ( target . output , target . require || { } , "./" ) ) ;
343+ let targetTime = await getCreationDateOfPath ( targetOutputPath ) ;
241344 // Step 2: let's find dependencies first and foremost.
242345 // these deps need to be built first.
346+ let depsModified = false ;
243347 for ( let dep of ( target . depends || [ ] ) ) {
244348 console . info ( `-> Building dependency "${ dep } "...` ) ;
245349 // let's look to see if this is a target or location
@@ -251,8 +355,10 @@ async function buildTarget() {
251355 primaryTarget = dep ;
252356 await buildTarget ( ) ;
253357 primaryTarget = currentTarget ;
358+ depsModified = depsModified || ! ! projectConfiguration . targets . find ( ( t : any ) => t . name === dep ) ?. wasModified ;
254359 continue ;
255360 }
361+ if ( depLocation ) depLocation . wasModified = false ;
256362 let collectedFiles = [ ]
257363 for ( let [ dir , buildFile ] of Object . entries ( buildFiles ) ) {
258364 dir = dir . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" )
@@ -301,6 +407,11 @@ async function buildTarget() {
301407 if ( ! fs . existsSync ( outputFile . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" ) ) ) {
302408 fs . mkdirSync ( outputFile . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" ) , { recursive : true } ) ;
303409 }
410+
411+ if ( ! ( await isOutputStale ( inputFile , outputFile ) ) ) {
412+ console . misc ( ` -> Skipping ${ inputFile } (cached)` ) ;
413+ continue ;
414+ }
304415 for ( let subcommand of command . build ) {
305416 if ( subcommand . type === "command" ) {
306417 let finalCommand = subcommand . command . replaceAll ( "$INPUT" , inputFile ) . replaceAll ( "$OUTPUT" , outputFile ) ;
@@ -354,6 +465,7 @@ async function buildTarget() {
354465 }
355466 if ( projectConfiguration . locations . find ( ( { name} : any ) => name === depLocation . name ) . wasModified ) {
356467 projectConfiguration . targets . find ( ( { name} : any ) => name === primaryTarget ) . wasModified = true ;
468+ depsModified = true ;
357469
358470 let nextCommands = depLocation . build ( collectedFiles ) ;
359471 for ( let command of nextCommands ) {
@@ -383,8 +495,8 @@ async function buildTarget() {
383495 // Step 3: We built all the dependencies to the target
384496 // so now we build the actual target
385497 // very simple since targets are assumed to be commands only with no dynamics
386- if ( ! projectConfiguration . targets . find ( ( { name } : any ) => name === primaryTarget ) . wasModified ) {
387- console . misc ( ` -> Using cached build for target "${ primaryTarget } "` ) ;
498+ if ( ! depsModified && fs . existsSync ( targetOutputPath ) ) {
499+ console . misc ( ` -> Using cached build for target "${ primaryTarget } " (deps unchanged) ` ) ;
388500 return ;
389501 }
390502 let targetPrerequisites : any = { } ;
@@ -399,6 +511,7 @@ async function buildTarget() {
399511 targetPrerequisites [ prereqVar ] = prereqResult . stdout . trim ( ) ;
400512 }
401513 }
514+ target . wasModified = true ;
402515 for ( let command of target . build ) {
403516 let commandStr = escapeCommand ( command , target . require , "./" ) ;
404517 for ( let [ prereqVar , prereqValue ] of Object . entries ( targetPrerequisites ) as [ string , string ] [ ] ) {
0 commit comments