@@ -19,93 +19,60 @@ import PackageGraph
1919import Workspace
2020import SPMBuildCore
2121import ArgumentParserToolInfo
22+ import SymbolKit
2223
2324extension CommandInfoV0 {
24- var doccReferenceFileName : String {
25- doccReferenceTitle + " .md "
25+ func toSymbolGraph( ) -> SymbolGraph {
26+ return SymbolGraph (
27+ metadata: SymbolGraph . Metadata ( formatVersion: . init( major: 0 , minor: 6 , patch: 0 ) , generator: " SwiftPM " ) ,
28+ module: SymbolGraph . Module ( name: self . commandName, platform: . init( architecture: " arm64 " , vendor: nil , operatingSystem: . init( name: " macOS " ) , environment: nil ) ) ,
29+ symbols: toSymbols ( ) ,
30+ relationships: [ ]
31+ )
2632 }
2733
28- var doccReferenceDocumentTitle : String {
29- let parts = ( superCommands ?? [ ] ) + [ commandName]
30- return parts. joined ( separator: " . " ) . uppercased ( )
31- }
32-
33- var doccReferenceTitle : String {
34- let parts = ( superCommands ?? [ ] ) + [ commandName]
35- return parts. joined ( separator: " . " )
36- }
34+ func toSymbols( _ path: [ String ] = [ ] ) -> [ SymbolGraph . Symbol ] {
35+ var symbols : [ SymbolGraph . Symbol ] = [ ]
3736
38- var doccReferenceName : String {
39- let parts = ( superCommands ?? [ ] ) + [ commandName]
40- return parts. joined ( separator: " " )
41- }
42- }
37+ var myPath = path
38+ myPath. append ( self . commandName)
4339
44- extension CommandInfoV0 {
45- /// Recursively parses a command to generate markdown content that describes the command.
46- /// - Parameters:
47- /// - path: The path of subcommands from the root command.
48- /// - markdownStyle: The flavor of markdown to emit, either `docc` or `github`
49- /// - Returns: A multi-line markdown file that describes the command.
50- ///
51- /// If `path` is empty, it represents a top-level command.
52- /// Otherwise it's a subcommand, potentially recursive to multiple levels.
53- func toMarkdown( _ path: [ String ] ) -> String {
54- var result =
55- String ( repeating: " # " , count: path. count + 1 )
56- + " \( self . doccReferenceTitle) \n \n "
57-
58- // sets the max width for generating code blocks of content based
59- // on the style
60- let blockWrapLength : Int = 60
61-
62- if path. count == 0 {
63- result += " <!-- Generated by swift-argument-parser --> \n \n "
40+ guard myPath. last != " help " else {
41+ return [ ]
6442 }
6543
66- if let abstract = self . abstract {
67- result += " \( abstract) \n \n "
68- }
44+ var line = 0
45+ var docComments : SymbolGraph . LineList = if let abstract = self . abstract { . init ( [ SymbolGraph . LineList . Line ( text : abstract, range : nil ) ] ) } else { . init ( [ ] ) }
46+ line += 2
6947
7048 if let args = self . arguments, args. count != 0 {
71- result += " ``` \n "
72- let commandString = ( path + [ self . commandName] ) . joined ( separator: " " )
73- result +=
74- commandString
75- + self . usage (
76- startlength: commandString. count, wraplength: blockWrapLength)
77- result += " \n ``` \n \n "
78- }
49+ let commandString : String = myPath. joined ( separator: " " )
7950
80- if let discussion = self . discussion {
81- result += " \( discussion ) \n \n "
51+ docComments = . init ( docComments . lines + [ SymbolGraph . LineList . Line ( text : " ``` \n " + commandString + self . usage ( startlength : commandString . count , wraplength : 60 ) + " \n ``` " , range : nil ) ] ) // TODO parameterize the wrap length
52+ line += 2
8253 }
8354
84- if let args = self . arguments {
85- for arg in args {
86- guard arg. shouldDisplay else {
87- continue
88- }
89-
90- result += " - term ** \( arg. identity ( ) ) **: \n \n "
91-
92- if let abstract = arg. abstract {
93- result += " * \( abstract) * \n \n "
94- }
95- if let discussion = arg. discussion {
96- result += discussion + " \n \n "
97- }
98- result += " \n "
99- }
55+ if let discussion = self . discussion {
56+ docComments = . init( docComments. lines + ( discussion. split ( separator: " \n " ) . map ( { SymbolGraph . LineList. Line ( text: String ( $0) , range: nil ) } ) ) )
57+ line += 2
10058 }
10159
102- for subcommand in self . subcommands ?? [ ] {
103- result +=
104- subcommand. toMarkdown (
105- path + [ self . commandName] ) + " \n \n "
60+ // TODO: Maybe someday there will be command-line semantics for the symbols and then these can be declared with more sensible categories
61+ symbols. append ( SymbolGraph . Symbol (
62+ identifier: . init( precise: " s: \( myPath. joined ( separator: " " ) ) " , interfaceLanguage: " swift " ) ,
63+ names: . init( title: self . commandName, navigator: nil , subHeading: nil , prose: nil ) ,
64+ pathComponents: myPath,
65+ docComment: docComments,
66+ accessLevel: SymbolGraph . Symbol. AccessControl ( rawValue: " public " ) ,
67+ kind: SymbolGraph . Symbol. Kind ( parsedIdentifier: . `func`, displayName: " command " ) ,
68+ mixins: [ : ]
69+ ) )
70+
71+ for cmd in self . subcommands ?? [ ] {
72+ symbols. append ( contentsOf: cmd. toSymbols ( myPath) )
10673 }
10774
108- return result
75+ return symbols
10976 }
11077
11178 /// Returns a mutl-line string that presents the arguments for a command.
@@ -221,9 +188,10 @@ struct GenerateDocumentation: AsyncSwiftCommand {
221188 var globalOptions : GlobalOptions
222189
223190 func run( _ swiftCommandState: SwiftCommandState ) async throws {
191+ // TODO someday we might be able to populate the landing page with details about the package as a whole, such as traits, or even a DocC catalog that covers package-level topics
192+
224193 let buildSystem = try await swiftCommandState. createBuildSystem ( )
225194
226- // TODO build only the product related targets when not generating internal docs
227195 let outputs = try await buildSystem. build ( subset: . allExcludingTests, buildOutputs: [
228196 . symbolGraph(
229197 . init(
@@ -267,105 +235,110 @@ struct GenerateDocumentation: AsyncSwiftCommand {
267235 }
268236 } else {
269237 modules. append ( contentsOf: rootPackage. modules)
238+ products. append ( contentsOf: rootPackage. products)
270239 }
271240 }
272241
273242 for product in products {
274243 if product. type == . executable {
275- let exec = builtArtifacts. filter ( { $0. 1 . kind == . executable && $0. 0 == " \( product. name) -product " } ) . first? . 1 . path
276-
277- guard let exec else {
278- print ( " Couldn't find \( product. name) " )
279- continue
244+ let doccCatalogDir = product. modules. first? . underlying. others. filter ( { $0. extension? . lowercased ( ) == " docc " } ) . first
245+ var symbolGraphDir : AbsolutePath ? = nil
246+
247+ if let exec = builtArtifacts. filter ( { $0. 1 . kind == . executable && $0. 0 == " \( product. name) -product " } ) . first? . 1 . path {
248+ do {
249+ // FIXME run the executable within a very restricted sandbox
250+ let dumpHelpProcess = AsyncProcess ( args: [ exec, " --experimental-dump-help " ] , outputRedirection: . collect)
251+ try dumpHelpProcess. launch ( )
252+ let result = try await dumpHelpProcess. waitUntilExit ( )
253+ let output = try result. utf8Output ( )
254+ let toolInfo = try JSONDecoder ( ) . decode ( ToolInfoV0 . self, from: output)
255+
256+ // Creating a symbol graph that represents the command-line structure
257+ symbolGraphDir = buildPath. appending ( components: [ " tool-symbol-graph " , product. name] )
258+ guard let graphDir = symbolGraphDir else { fatalError ( ) }
259+
260+ try ? swiftCommandState. fileSystem. removeFileTree ( graphDir)
261+ try swiftCommandState. fileSystem. createDirectory ( graphDir, recursive: true )
262+
263+ let graph = toolInfo. command. toSymbolGraph ( )
264+ let doc = try JSONEncoder ( ) . encode ( graph)
265+ let graphFile = graphDir. appending ( components: [ " \( product. name) .symbols.json " ] )
266+ try swiftCommandState. fileSystem. writeFileContents ( graphFile, data: doc)
267+ } catch {
268+ print ( " warning: could not generate tool info documentation for \( product. name) " )
280269 }
281-
282- do {
283- // FIXME run the executable within a very restricted sandbox
284- let dumpHelpProcess = AsyncProcess ( args: [ exec, " --experimental-dump-help " ] , outputRedirection: . collect)
285- try dumpHelpProcess. launch ( )
286- let result = try await dumpHelpProcess. waitUntilExit ( )
287- let output = try result. utf8Output ( )
288- let toolInfo = try JSONDecoder ( ) . decode ( ToolInfoV0 . self, from: output)
289-
290- let page = toolInfo. command. toMarkdown ( [ ] )
291-
292- // Creating a simple DocC catalog
293- // TODO copy over an existing DocC catalog for this module if one exists already
294- let catalogDir = buildPath. appending ( components: [ " tool-docc-catalog " , product. name, " Documentation.docc " ] )
295- try ? swiftCommandState. fileSystem. removeFileTree ( catalogDir)
296- try swiftCommandState. fileSystem. createDirectory ( catalogDir, recursive: true )
297- let summaryMd = catalogDir. appending ( components: [ " \( product. name. lowercased ( ) ) .md " ] )
298- try swiftCommandState. fileSystem. writeFileContents ( summaryMd, string: page)
299-
300- let archiveDir = buildPath. appending ( components: [ " tool-docc-archive " , " \( product. name) .doccarchive " ] )
301- try ? swiftCommandState. fileSystem. removeFileTree ( archiveDir)
302- try swiftCommandState. fileSystem. createDirectory ( archiveDir. parentDirectory, recursive: true )
303-
304- print ( " CONVERT TOOL: \( product. name) " )
305-
306- let process = try Process . run ( URL ( fileURLWithPath: doccExecutable. pathString) , arguments: [
307- " convert " ,
308- catalogDir. pathString,
309- " --fallback-display-name= \( product. name) " ,
310- " --fallback-bundle-identifier= \( product. name) " ,
311- " --output-path= \( archiveDir) " ,
312- ] )
313- process. waitUntilExit ( )
314-
315- if swiftCommandState. fileSystem. exists ( archiveDir) {
316- doccArchives. append ( archiveDir. pathString)
317- }
318- } catch {
319- print ( " warning: could not generate tool info documentation for \( product. name) " )
320- }
321-
322- continue
323270 }
324- }
325271
326- for module : ResolvedModule in modules {
327- guard module . type != . test && module . type != . plugin && module . type != . executable else {
328- continue
272+ guard doccCatalogDir != nil || symbolGraphDir != nil else {
273+ print ( " Skipping \( product . name ) because there is no DocC catalog and there is no symbol graph that could be generated for it. " )
274+ continue
329275 }
330276
331- let inputPathDir = symbolGraph . outputLocationForTarget ( module . name , try swiftCommandState . productsBuildParameters )
332- let inputPath = buildPath . appending ( components : inputPathDir )
277+ let catalogArgs = if let doccCatalogDir { [ doccCatalogDir . pathString ] } else { [ String ] ( ) }
278+ let graphArgs = if let symbolGraphDir { [ " --additional-symbol-graph-dir= \( symbolGraphDir ) " ] } else { [ String ] ( ) }
333279
334- guard swiftCommandState. fileSystem. exists ( inputPath) else {
335- continue
280+ print ( " CONVERTING: \( product. name) " )
281+
282+ let archiveDir = buildPath. appending ( components: [ " tool-docc-archive " , " \( product. name) .doccarchive " ] )
283+ try ? swiftCommandState. fileSystem. removeFileTree ( archiveDir)
284+ try swiftCommandState. fileSystem. createDirectory ( archiveDir. parentDirectory, recursive: true )
285+
286+ let process = try Process . run ( URL ( fileURLWithPath: doccExecutable. pathString) , arguments: [
287+ " convert " ,
288+ ] + catalogArgs + [
289+ " --fallback-display-name= \( product. name) " ,
290+ " --fallback-bundle-identifier= \( product. name) " ,
291+ ] + graphArgs + [
292+ " --output-path= \( archiveDir) " ,
293+ ] )
294+ process. waitUntilExit ( )
295+
296+ if swiftCommandState. fileSystem. exists ( archiveDir) {
297+ print ( " SUCCESS! " )
298+ doccArchives. append ( archiveDir. pathString)
336299 }
300+ }
301+ }
337302
338- let outputPathDir = [ String] ( inputPathDir. dropLast ( ) ) + [ inputPathDir. last!. replacing ( " .symbolgraphs " , with: " .doccarchive " ) ]
339- let outputPath = buildPath. appending ( components: outputPathDir)
303+ for module : ResolvedModule in modules {
304+ let symbolGraphDir = symbolGraph. outputLocationForTarget ( module. name, try swiftCommandState. productsBuildParameters)
305+ let symbolGraphPath = buildPath. appending ( components: symbolGraphDir)
340306
341307 // The DocC catalog for this module is any directory with the docc file extension
342- let doccCatalog = module. underlying. others. first { sourceFile in
308+ let doccCatalogDir = module. underlying. others. first { sourceFile in
343309 return sourceFile. extension? . lowercased ( ) == " docc "
344310 }
345311
346- let catalogArgs = if let doccCatalog { [ doccCatalog. pathString] } else { [ String] ( ) }
312+ guard doccCatalogDir != nil || swiftCommandState. fileSystem. exists ( symbolGraphPath) else {
313+ print ( " Skipping \( module. name) because there is no DocC catalog and there is no symbol graph that could be generated for it. " )
314+ continue
315+ }
347316
348- print ( " CONVERT: \( module. name) " )
317+ let catalogArgs = if let doccCatalogDir { [ doccCatalogDir. pathString] } else { [ String] ( ) }
318+ let graphArgs = if swiftCommandState. fileSystem. exists ( symbolGraphPath) { [ " --additional-symbol-graph-dir= \( symbolGraphPath) " ] } else { [ String] ( ) }
319+
320+ print ( " CONVERTING: \( module. name) " )
321+
322+ let archiveDir = buildPath. appending ( components: [ " module-docc-archive " , " \( module. name) .doccarchive " ] )
323+ try ? swiftCommandState. fileSystem. removeFileTree ( archiveDir)
324+ try swiftCommandState. fileSystem. createDirectory ( archiveDir. parentDirectory, recursive: true )
349325
350- print ( " docc convert \( catalogArgs. joined ( separator: " " ) ) \( [ " --fallback-display-name= \( module. name) " , " --fallback-bundle-identifier= \( module. name) " , " --additional-symbol-graph-dir= \( inputPath) " , " --output-path= \( outputPath) " ] . joined ( separator: " " ) ) " )
351-
352326 let process = try Process . run ( URL ( fileURLWithPath: doccExecutable. pathString) , arguments: [
353327 " convert " ,
354328 ] + catalogArgs + [
355329 " --fallback-display-name= \( module. name) " ,
356330 " --fallback-bundle-identifier= \( module. name) " ,
357- " --additional-symbol-graph-dir= \( inputPath ) " ,
358- " --output-path= \( outputPath ) " ,
331+ ] + graphArgs + [
332+ " --output-path= \( archiveDir ) " ,
359333 ] )
360334 process. waitUntilExit ( )
361335
362- if swiftCommandState. fileSystem. exists ( outputPath ) {
363- doccArchives. append ( outputPath . pathString)
336+ if swiftCommandState. fileSystem. exists ( archiveDir ) {
337+ doccArchives. append ( archiveDir . pathString)
364338 }
365339 }
366340
367341 guard doccArchives. count > 0 else {
368- // FIXME consider presenting just the README.md contents if possible
369342 print ( " No modules are available to document. " )
370343 return
371344 }
@@ -376,7 +349,7 @@ struct GenerateDocumentation: AsyncSwiftCommand {
376349 try ? swiftCommandState. fileSystem. removeFileTree ( outputPath) // docc merge requires an empty output directory
377350 try swiftCommandState. fileSystem. createDirectory ( outputPath, recursive: true )
378351
379- print ( " MERGE " )
352+ print ( " MERGE: \( doccArchives ) " )
380353
381354 let process = try Process . run ( URL ( fileURLWithPath: doccExecutable. pathString) , arguments: [
382355 " merge " ,
@@ -390,7 +363,5 @@ struct GenerateDocumentation: AsyncSwiftCommand {
390363 // TODO provide an option to set up an http server
391364 print ( " python3 -m http.server --directory \( outputPath) " )
392365 print ( " http://localhost:8000/documentation " )
393-
394- // TODO figure out how to monitor for changes in preview mode so that it automatically updates itself (perhaps sourcekit-lsp/vscode is a much better way forward)
395- }
366+ }
396367}
0 commit comments