11import { existsSync } from 'node:fs'
22
3- import {
4- type ResolvedArtifactPaths ,
5- type SocketFactsSbom ,
6- type SocketFactsSbomComponent ,
7- type SocketFactsSbomDependency ,
8- type SocketFactsSbomMetadata ,
9- type SocketFactsSbomProject ,
10- } from './facts.mts'
113import { moduleKey } from './resolution-report.mts'
124
5+ import type {
6+ ClasspathEntry ,
7+ OutputPaths ,
8+ ResolvedArtifactPaths ,
9+ SocketFactsSbom ,
10+ SocketFactsSbomComponent ,
11+ SocketFactsSbomDependency ,
12+ SocketFactsSbomMetadata ,
13+ SocketFactsSbomProject ,
14+ } from './facts.mts'
15+
1316import type {
1417 ParsedRecords ,
1518 RawCoord ,
1619 RawProject ,
1720 RawRoot ,
21+ StatedClasspathEntry ,
1822} from './records.mts'
1923import type { ResolutionReport } from './resolution-report.mts'
2024
@@ -49,8 +53,9 @@ type SubprojectGraph = {
4953
5054type PerRoot = {
5155 projectKey : string
52- config : string
5356 prod : boolean
57+ outputSet : string
58+ role : RawRoot [ 'role' ]
5459 // In the root's classpath order.
5560 nodes : Map < string , RootNode >
5661}
@@ -60,6 +65,7 @@ type RootNode = {
6065 children : string [ ]
6166 direct : boolean
6267 project : string
68+ outputSets : string [ ]
6369 targets : string [ ]
6470 unfetched : boolean
6571}
@@ -127,11 +133,17 @@ function buildPerRoot(parsed: ParsedRecords): Map<string, PerRoot> {
127133 children : [ ] ,
128134 direct : false ,
129135 project : n . project ,
136+ outputSets : [ ] ,
130137 targets : [ ] ,
131138 unfetched : false ,
132139 }
133140 nodes . set ( id , node )
134141 }
142+ for ( const set of n . outputSets ) {
143+ if ( ! node . outputSets . includes ( set ) ) {
144+ node . outputSets . push ( set )
145+ }
146+ }
135147 node . direct ||= n . direct
136148 node . unfetched ||= n . unfetched
137149 node . targets . push ( ...n . targets )
@@ -149,16 +161,17 @@ function buildPerRoot(parsed: ParsedRecords): Map<string, PerRoot> {
149161 }
150162 out . set ( rootId , {
151163 projectKey : r . projectKey ,
152- config : r . config ,
153164 prod : r . prod ,
165+ outputSet : r . outputSet ,
166+ role : r . role ,
154167 nodes,
155168 } )
156169 }
157170 return out
158171}
159172
160173// Components are merged by id across every resolution root; which ids belong
161- // to which subproject is kept separately (classpathByProject ) for
174+ // to which subproject is kept separately (classpathsByProject ) for
162175// reachability.
163176function mergeById ( perRoot : Map < string , PerRoot > ) : Map < string , MergedNode > {
164177 const finalNodes = new Map < string , MergedNode > ( )
@@ -408,50 +421,51 @@ function treeRoots(
408421 return roots
409422}
410423
411- // A test configuration: Gradle's `test…`/`…Test…` (testRuntimeClasspath,
412- // integrationTestCompileClasspath, debugUnitTestRuntimeClasspath), sbt's
413- // test and it configurations.
414- const TEST_CONFIG = / ^ t e s t | T e s t | ^ i t (?: - i n t e r n a l ) ? $ /
415- const SBT_TEST_CONFIG = / ^ (?: t e s t | i t ) (?: - i n t e r n a l ) ? $ /
416-
417- // Lower ranks go first; an unrecognised configuration counts as tooling.
418- function classpathRank ( config : string ) : number {
419- const n = config . toLowerCase ( )
420- // Gradle's legacy `runtime`/`compile` (before Gradle 7) are a subset of the
421- // matching `…Classpath` configuration, so they rank just behind it.
422- const legacy = n . endsWith ( 'runtime' ) || n . endsWith ( 'compile' ) ? 0.5 : 0
423- const runtime = n . endsWith ( 'runtimeclasspath' ) || n . endsWith ( 'runtime' )
424- const compile = n . endsWith ( 'compileclasspath' ) || n . endsWith ( 'compile' )
425- if ( TEST_CONFIG . test ( config ) ) {
426- if ( runtime || SBT_TEST_CONFIG . test ( n ) ) {
427- return 4 + legacy
428- }
429- return compile ? 5 + legacy : 6
430- }
431- if ( n === 'runtimeclasspath' || n === 'runtime' || n === 'runtime-internal' ) {
432- return legacy
433- }
434- if ( n === 'compileclasspath' || n === 'compile' || n === 'compile-internal' ) {
435- return 1 + legacy
436- }
437- if ( runtime ) {
438- return 2 + legacy
439- }
440- if ( compile || n === 'provided' || n === 'optional' ) {
441- return 3 + legacy
424+ // A dependency on a build project names the output sets it brings in; without
425+ // one it is a plain dependency, which brings the project's main code.
426+ function entriesOf (
427+ id : string ,
428+ project : string ,
429+ outputSets : string [ ] ,
430+ ) : ClasspathEntry [ ] {
431+ if ( ! project ) {
432+ return [ { id } ]
442433 }
443- return 6
434+ return ( outputSets . length ? outputSets : [ 'main' ] ) . map ( outputs => ( {
435+ project : id ,
436+ outputs,
437+ } ) )
444438}
445439
446- // The JVM loads a class from the first classpath entry that has it, so the
447- // first root by rank to resolve a module fixes its version.
448- function deriveClasspath ( roots : PerRoot [ ] ) : string [ ] {
449- const ranked = [ ...roots ] . sort (
450- ( a , b ) => classpathRank ( a . config ) - classpathRank ( b . config ) ,
440+ // The project's own code goes first, the way the build tools put it.
441+ function ownEntries ( project : RawProject , outputSet : string ) : ClasspathEntry [ ] {
442+ return [ outputSet , ...( project . outputSets . get ( outputSet ) ?? [ ] ) ] . map (
443+ outputs => ( { project : project . projectKey , outputs } ) ,
451444 )
445+ }
446+
447+ function uniqueEntries ( entries : ClasspathEntry [ ] ) : ClasspathEntry [ ] {
448+ const seen = new Set < string > ( )
449+ return entries . filter ( e => {
450+ const key = 'id' in e ? e . id : `${ e . project } \u0000${ e . outputs } `
451+ if ( seen . has ( key ) ) {
452+ return false
453+ }
454+ seen . add ( key )
455+ return true
456+ } )
457+ }
458+
459+ // The runtime classpath, then the entries only the compile classpath has
460+ // (provided, compileOnly), in the version the runtime one resolved, if any.
461+ function derivedClasspath ( roots : PerRoot [ ] ) : ClasspathEntry [ ] {
462+ const ordered = [
463+ ...roots . filter ( r => r . role === 'runtime' ) ,
464+ ...roots . filter ( r => r . role === 'compile' ) ,
465+ ]
452466 const versionOf = new Map < string , string > ( )
453- const out = new Set < string > ( )
454- for ( const { nodes } of ranked ) {
467+ const out : ClasspathEntry [ ] = [ ]
468+ for ( const { nodes } of ordered ) {
455469 for ( const { 0 : id , 1 : node } of nodes ) {
456470 const module = node . project
457471 ? `project ${ node . project } `
@@ -462,42 +476,33 @@ function deriveClasspath(roots: PerRoot[]): string[] {
462476 versionOf . set ( module , version )
463477 }
464478 if ( version === node . coord . version ) {
465- out . add ( id )
479+ out . push ( ... entriesOf ( id , node . project , node . outputSets ) )
466480 }
467481 }
468482 }
469- return [ ... out ]
483+ return out
470484}
471485
472- // A project's classpath as its build tool states it, by component id.
473486function statedClasspath (
474487 projectKey : string ,
475- stated : string [ ] ,
476- roots : RawRoot [ ] ,
477- ) : string [ ] {
478- const ids = new Map < string , string > ( )
479- for ( const r of roots ) {
480- for ( const { 0 : coordId , 1 : n } of r . nodes ) {
481- ids . set ( coordId , componentId ( coordId , n . project ) )
482- }
483- }
484- const cp = new Set < string > ( )
485- for ( const coordId of stated ) {
486- const id = ids . get ( coordId )
487- if ( id === undefined ) {
488+ stated : StatedClasspathEntry [ ] ,
489+ projectOf : Map < string , string > ,
490+ ) : ClasspathEntry [ ] {
491+ return stated . flatMap ( ( { coordId, outputSets } ) => {
492+ const project = projectOf . get ( coordId )
493+ if ( project === undefined ) {
488494 throw new Error (
489495 `The classpath of ${ projectKey } names ${ coordId } , which none of its resolution roots resolved` ,
490496 )
491497 }
492- cp . add ( id )
493- }
494- return [ ...cp ]
498+ return entriesOf ( componentId ( coordId , project ) , project , outputSets )
499+ } )
495500}
496501
497502function buildClasspaths (
498503 parsed : ParsedRecords ,
499504 perRoot : Map < string , PerRoot > ,
500- ) : Map < string , string [ ] > {
505+ ) : Map < string , Map < string , ClasspathEntry [ ] > > {
501506 const rootIdsByProject = new Map < string , string [ ] > ( )
502507 for ( const { 0 : rootId , 1 : r } of parsed . roots ) {
503508 let list = rootIdsByProject . get ( r . projectKey )
@@ -507,20 +512,50 @@ function buildClasspaths(
507512 }
508513 list . push ( rootId )
509514 }
510- const out = new Map < string , string [ ] > ( )
511- for ( const projectKey of parsed . projects . keys ( ) ) {
515+ const out = new Map < string , Map < string , ClasspathEntry [ ] > > ( )
516+ for ( const { 0 : projectKey , 1 : project } of parsed . projects ) {
512517 const rootIds = rootIdsByProject . get ( projectKey ) ?? [ ]
513- const stated = parsed . classpaths . get ( projectKey )
514- out . set (
515- projectKey ,
516- stated
517- ? statedClasspath (
518- projectKey ,
519- stated ,
520- rootIds . flatMap ( id => parsed . roots . get ( id ) ?? [ ] ) ,
521- )
522- : deriveClasspath ( rootIds . flatMap ( id => perRoot . get ( id ) ?? [ ] ) ) ,
518+ // Every output set has a classpath, if only its own code.
519+ const classpaths = new Map < string , ClasspathEntry [ ] > (
520+ [ ...project . outputSets . keys ( ) ] . map ( name => [ name , [ ] ] ) ,
523521 )
522+ const stated = parsed . classpaths . get ( projectKey )
523+ if ( stated ) {
524+ const projectOf = new Map < string , string > ( )
525+ for ( const id of rootIds ) {
526+ for ( const { 0 : coordId , 1 : n } of parsed . roots . get ( id ) ?. nodes ?? [ ] ) {
527+ projectOf . set ( coordId , n . project )
528+ }
529+ }
530+ for ( const { 0 : name , 1 : entries } of stated ) {
531+ classpaths . set ( name , statedClasspath ( projectKey , entries , projectOf ) )
532+ }
533+ } else {
534+ const bySet = new Map < string , PerRoot [ ] > ( )
535+ for ( const id of rootIds ) {
536+ const r = perRoot . get ( id )
537+ if ( r ?. outputSet && r . role ) {
538+ let roots = bySet . get ( r . outputSet )
539+ if ( ! roots ) {
540+ roots = [ ]
541+ bySet . set ( r . outputSet , roots )
542+ }
543+ roots . push ( r )
544+ }
545+ }
546+ for ( const { 0 : name , 1 : roots } of bySet ) {
547+ if ( roots . some ( r => r . role === 'runtime' ) ) {
548+ classpaths . set ( name , derivedClasspath ( roots ) )
549+ }
550+ }
551+ }
552+ for ( const { 0 : name , 1 : entries } of classpaths ) {
553+ classpaths . set (
554+ name ,
555+ uniqueEntries ( [ ...ownEntries ( project , name ) , ...entries ] ) ,
556+ )
557+ }
558+ out . set ( projectKey , classpaths )
524559 }
525560 return out
526561}
@@ -530,7 +565,7 @@ function buildArtifactPaths(
530565 projects : RawProject [ ] ,
531566 excludedProjects : RawProject [ ] ,
532567 graphs : Map < string , SubprojectGraph > ,
533- classpaths : Map < string , string [ ] > ,
568+ classpaths : Map < string , Map < string , ClasspathEntry [ ] > > ,
534569 fileExists : ( path : string ) => boolean ,
535570) : ResolvedArtifactPaths {
536571 const pathsById : ResolvedArtifactPaths [ 'pathsById' ] = new Map ( )
@@ -554,25 +589,28 @@ function buildArtifactPaths(
554589 pathsById . set (
555590 id ,
556591 fn . resolutions . length && ! complete . length
557- ? { sources : [ ] }
558- : {
559- sources : [ ] ,
560- targets : [ ...new Set ( complete . flatMap ( r => r . targets ) ) ] . sort ( ) ,
561- } ,
592+ ? { }
593+ : { targets : [ ...new Set ( complete . flatMap ( r => r . targets ) ) ] . sort ( ) } ,
562594 )
563595 }
564596 }
565597 // A project's own component shares its id, so this also covers dependency
566598 // edges onto a sibling project.
599+ const outputsByProject = new Map < string , Map < string , OutputPaths > > ( )
567600 for ( const p of [ ...projects , ...excludedProjects ] ) {
568- pathsById . set ( p . projectKey , {
569- sources : [ ...new Set ( p . sources ) ] . filter ( fileExists ) . sort ( ) ,
570- targets : [ ...new Set ( p . targets ) ] . filter ( fileExists ) . sort ( ) ,
571- } )
601+ const sets = new Map < string , OutputPaths > ( )
602+ for ( const { 0 : name , 1 : o } of p . outputs ) {
603+ sets . set ( name , {
604+ sources : [ ...new Set ( o . sources ) ] . filter ( fileExists ) . sort ( ) ,
605+ targets : [ ...new Set ( o . targets ) ] . filter ( fileExists ) . sort ( ) ,
606+ } )
607+ }
608+ outputsByProject . set ( p . projectKey , sets )
572609 }
573610 return {
574611 pathsById,
575- classpathByProject : classpaths ,
612+ outputsByProject,
613+ classpathsByProject : classpaths ,
576614 directDependenciesByProject : new Map (
577615 projects . map ( p => {
578616 const g = graphs . get ( p . projectKey )
0 commit comments