@@ -44,12 +44,10 @@ class Lens extends Configurable {
4444 const config = await super . getConfig ( ) ;
4545
4646 // process lens configuration
47- if ( ! config . package ) {
48- throw new Error ( `hololens has no package defined: ${ this . name } ` ) ;
47+ if ( config . package ) {
48+ config . command = config . command || 'lens-tree {{ input }}' ;
4949 }
5050
51- config . command = config . command || 'lens-tree {{ input }}' ;
52-
5351 if ( config . before ) {
5452 config . before =
5553 typeof config . before == 'string'
@@ -103,7 +101,62 @@ class Lens extends Configurable {
103101 async buildSpec ( inputTree ) {
104102 const config = await this . getCachedConfig ( ) ;
105103
104+ if ( config . container ) {
105+ return this . buildSpecForContainer ( inputTree , config ) ;
106+ } else if ( config . package ) {
107+ return this . buildSpecForHabitatPackage ( inputTree , config ) ;
108+ } else {
109+ throw new Error ( `hololens has no package or container defined: ${ this . name } ` ) ;
110+ }
111+ }
112+
113+ async buildSpecForContainer ( inputTree , config ) {
114+ const { container : containerQuery } = config ;
115+
116+ // check if image exists locally first
117+ let imageHash ;
118+ try {
119+ const inspectOutput = await Studio . execDocker ( [ 'inspect' , containerQuery ] ) ;
120+ const imageInfo = JSON . parse ( inspectOutput ) [ 0 ] ;
121+ imageHash = imageInfo . Id ;
122+ logger . info ( `found local image: ${ containerQuery } @${ imageHash } ` ) ;
123+ } catch ( err ) {
124+ // image doesn't exist locally or can't be inspected, try pulling
125+ logger . info ( `pulling image: ${ containerQuery } ` ) ;
126+
127+ try {
128+ await Studio . execDocker ( [ 'pull' , containerQuery ] , { $relayStdout : true } ) ;
129+ const inspectOutput = await Studio . execDocker ( [ 'inspect' , containerQuery ] ) ;
130+ const imageInfo = JSON . parse ( inspectOutput ) [ 0 ] ;
131+ imageHash = imageInfo . Id ;
132+ } catch ( err ) {
133+ throw new Error ( `failed to pull container image ${ containerQuery } : ${ err . message } ` ) ;
134+ }
135+ }
136+
137+ if ( ! imageHash ) {
138+ throw new Error ( `failed to get hash for container image ${ containerQuery } ` ) ;
139+ }
140+
141+ // build spec
142+ const data = {
143+ ...config ,
144+ container : `${ containerQuery . replace ( / : .* $ / , '' ) } @${ imageHash } ` ,
145+ input : await inputTree . write ( ) ,
146+ output : null ,
147+ before : null ,
148+ after : null
149+ } ;
106150
151+ // write spec and return packet
152+ return {
153+ ...await SpecObject . write ( this . workspace . getRepo ( ) , 'lens' , data ) ,
154+ data,
155+ type : 'container'
156+ } ;
157+ }
158+
159+ async buildSpecForHabitatPackage ( inputTree , config ) {
107160 // determine current package version
108161 const { package : packageQuery } = config ;
109162 const [ pkgOrigin , pkgName , pkgVersion , pkgBuild ] = packageQuery . split ( '/' ) ;
@@ -163,14 +216,6 @@ class Lens extends Configurable {
163216 }
164217
165218
166- // old studio method that might be useful as fallback/debug option
167- // const setupOutput = await studio.exec('hab', 'pkg', 'install', 'core/hab-plan-build');
168- // const originOutput = await studio.exec('hab', 'origin', 'key', 'generate', 'holo');
169- // const buildOutput = await studio.habPkgExec('core/hab-plan-build', 'hab-plan-build', '/src/lenses/compass');
170- // const studio = await Studio.get(this.workspace.getRepo().gitDir);
171- // let packageIdent = await studio.getPackage(packageQuery);
172-
173-
174219 // build spec
175220 const data = {
176221 ...config ,
@@ -185,21 +230,21 @@ class Lens extends Configurable {
185230 // write spec and return packet
186231 return {
187232 ...await SpecObject . write ( this . workspace . getRepo ( ) , 'lens' , data ) ,
188- data
233+ data,
234+ type : 'habitat'
189235 } ;
190236 }
191237
192- async executeSpec ( specHash , options ) {
193- return Lens . executeSpec ( specHash , { ...options , repo : this . workspace . getRepo ( ) } ) ;
238+ async executeSpec ( specType , specHash , options ) {
239+ return Lens . executeSpec ( specType , specHash , { ...options , repo : this . workspace . getRepo ( ) } ) ;
194240 }
195241
196- static async executeSpec ( specHash , { refresh= false , save= true , repo= null , cacheFrom= null , cacheTo= null } ) {
242+ static async executeSpec ( specType , specHash , options ) {
243+ const { refresh= false , cacheFrom= null , cacheTo= null , save= true } = options ;
197244
198- // load holorepo
199- if ( ! repo ) {
200- repo = await Repo . getFromEnvironment ( ) ;
201- }
202245
246+ // load holorepo
247+ const repo = options . repo || await Repo . getFromEnvironment ( ) ;
203248 const git = await repo . getGit ( ) ;
204249
205250
@@ -228,9 +273,161 @@ class Lens extends Configurable {
228273 }
229274
230275
231- // ensure the rest runs inside a studio environment
276+ // execute lens in container or with habitat package:
277+ let lensedTreeHash ;
278+ if ( specType == 'container' ) {
279+ lensedTreeHash = await Lens . executeSpecForContainer ( repo , specHash ) ;
280+ } else if ( specType == 'habitat' ) {
281+ lensedTreeHash = await Lens . executeSpecForHabitatPackage ( repo , specHash ) ;
282+ }
283+
284+ // save ref to accelerate next projection
285+ if ( save ) {
286+ await git . updateRef ( specRef , lensedTreeHash ) ;
287+
288+ if ( cacheTo ) {
289+ await _cacheResultTo ( repo , specRef , cacheTo ) ;
290+ }
291+ }
292+
293+ return lensedTreeHash ;
294+ }
295+
296+ static async executeSpecForContainer ( repo , specHash ) {
297+ const git = await repo . getGit ( ) ;
298+
299+ // read and parse spec file
300+ const specToml = await git . catFile ( { p : true } , specHash ) ;
301+ const {
302+ holospec : {
303+ lens : spec
304+ }
305+ } = TOML . parse ( specToml ) ;
306+
307+ // write commit with input tree and spec content
308+ const commitHash = await git . commitTree ( spec . input , {
309+ p : [ ] ,
310+ m : specToml
311+ } ) ;
312+
313+ // extract repository and hash from container string
314+ const containerMatch = spec . container . match ( / ^ .+ @ s h a 2 5 6 : ( [ a - f 0 - 9 ] { 64 } ) $ / ) ;
315+ if ( ! containerMatch ) {
316+ throw new Error ( `Invalid container format: ${ spec . container } ` ) ;
317+ }
318+ const [ , sha256Hash ] = containerMatch ;
319+
320+ // create and start container
321+ const persistentDebugContainer = process . env . HOLO_DEBUG_PERSIST_CONTAINER ;
322+ let containerId ;
323+ try {
324+ if ( persistentDebugContainer ) {
325+ try {
326+ const containerInfo = await Studio . execDocker ( [ 'inspect' , persistentDebugContainer ] ) ;
327+ const containerState = JSON . parse ( containerInfo ) [ 0 ] . State ;
328+
329+ if ( containerState . Running ) {
330+ logger . info ( `Found running debug container: ${ persistentDebugContainer } ` ) ;
331+ containerId = persistentDebugContainer ;
332+ }
333+ } catch ( error ) {
334+ containerId = null ;
335+ }
336+ }
337+
338+ // create container
339+ if ( ! containerId ) {
340+ containerId = await Studio . execDocker ( [
341+ 'create' ,
342+ '-p' , '9000:9000' ,
343+ ...( persistentDebugContainer ? [ '--name' , persistentDebugContainer ] : [ ] ) ,
344+ ...( process . env . DEBUG ? [ '-e' , 'DEBUG=1' ] : [ ] ) ,
345+ sha256Hash
346+ ] ) ;
347+ containerId = containerId . trim ( ) ;
348+
349+ logger . info ( 'starting container' ) ;
350+ await Studio . execDocker ( [ 'start' , containerId ] ) ;
351+ }
352+
353+ // wait for port 9000 to be available
354+ let attempts = 0 ;
355+ const maxAttempts = 30 ;
356+ const waitTime = 1000 ; // 1 second
357+
358+ while ( attempts < maxAttempts ) {
359+ try {
360+ const containerInfo = await Studio . execDocker ( [ 'inspect' , containerId ] ) ;
361+ const containerState = JSON . parse ( containerInfo ) [ 0 ] . State ;
362+
363+ if ( containerState . Running ) {
364+ // check if port 9000 is listening
365+ try {
366+ await Studio . execDocker ( [
367+ 'exec' ,
368+ containerId ,
369+ 'nc' ,
370+ '-z' ,
371+ 'localhost' ,
372+ '9000'
373+ ] ) ;
374+ break ;
375+ } catch ( err ) {
376+ // ignore error and continue waiting
377+ }
378+ }
379+ } catch ( err ) {
380+ // ignore error and continue waiting
381+ }
382+
383+ await new Promise ( resolve => setTimeout ( resolve , waitTime ) ) ;
384+ attempts ++ ;
385+ }
386+
387+ if ( attempts >= maxAttempts ) {
388+ throw new Error ( 'Timeout waiting for git server to be ready' ) ;
389+ }
390+
391+ // push commit to git server
392+ logger . info ( `pushing and executing job: ${ commitHash } ` ) ;
393+ await git . push ( `http://localhost:9000/` , `${ commitHash } :refs/heads/lens-input` , {
394+ force : true ,
395+ $wait : true ,
396+ $onStderr : ( line ) => process . stderr . write ( `\x1b[90m${ line } \x1b[0m\n` )
397+ } ) ;
398+
399+ // fetch and verify output commit
400+ const outputRef = `refs/lens-jobs/${ specHash } ` ;
401+ logger . info ( 'fetching result' ) ;
402+ await git . fetch ( 'http://localhost:9000/' , `+refs/heads/lens-input:${ outputRef } ` ) ;
403+
404+ // verify the output commit's parent matches our input commit
405+ const outputParent = await git . revParse ( `${ outputRef } ^` ) ;
406+ if ( outputParent !== commitHash ) {
407+ throw new Error ( `Output commit parent ${ outputParent } does not match input commit ${ commitHash } ` ) ;
408+ }
409+
410+ return await git . getTreeHash ( outputRef ) ;
411+
412+ } finally {
413+ // cleanup
414+ if ( containerId && ! persistentDebugContainer ) {
415+ try {
416+ await Studio . execDocker ( [ 'stop' , containerId ] ) ;
417+ await Studio . execDocker ( [ 'rm' , containerId ] ) ;
418+ } catch ( err ) {
419+ logger . warn ( `Failed to cleanup container: ${ err . message } ` ) ;
420+ }
421+ }
422+ }
423+ }
424+
425+ static async executeSpecForHabitatPackage ( repo , specHash ) {
426+ const git = await repo . getGit ( ) ;
427+
232428 let lensedTreeHash ;
233429
430+ // ensure the rest runs inside a studio environment
234431 if ( ! await Studio . isEnvironmentStudio ( ) ) {
235432 const studio = await Studio . get ( repo . gitDir ) ;
236433 lensedTreeHash = await studio . holoLensExec ( specHash ) ;
@@ -307,16 +504,6 @@ class Lens extends Configurable {
307504 }
308505
309506
310- // save ref to accelerate next projection
311- if ( save ) {
312- await git . updateRef ( specRef , lensedTreeHash ) ;
313-
314- if ( cacheTo ) {
315- await _cacheResultTo ( repo , specRef , cacheTo ) ;
316- }
317- }
318-
319-
320507 // return tree hash
321508 return lensedTreeHash ;
322509 }
0 commit comments