diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 00000000..66b43b86 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [SwiftlyDocs] diff --git a/Documentation/EmptyFile.swift b/Documentation/EmptyFile.swift new file mode 100644 index 00000000..bc73e58d --- /dev/null +++ b/Documentation/EmptyFile.swift @@ -0,0 +1,4 @@ +// This is an empty placeholder swift file for the documentation target. + +// You can preview the documentation here by running the following command: +// swift package --disable-sandbox preview-documentation --target SwiftlyDocs diff --git a/Documentation/SwiftlyDocs.docc/SwiftlyDocs.md b/Documentation/SwiftlyDocs.docc/SwiftlyDocs.md new file mode 100644 index 00000000..d719ff4b --- /dev/null +++ b/Documentation/SwiftlyDocs.docc/SwiftlyDocs.md @@ -0,0 +1,21 @@ +# ``SwiftlyDocs`` + +Install and manage your Swift programming language toolchains. + +@Metadata { + @DisplayName("Swiftly") +} + +## Topics + +- + +### HOWTOS + +- +- +- + +### Reference + +- diff --git a/Documentation/SwiftlyDocs.docc/getting-started.md b/Documentation/SwiftlyDocs.docc/getting-started.md new file mode 100644 index 00000000..ad41e3fa --- /dev/null +++ b/Documentation/SwiftlyDocs.docc/getting-started.md @@ -0,0 +1,43 @@ +# Getting Started with Swiftly + +To download swiftly and install Swift, run the following in your terminal, then follow the on-screen instructions: + +``` +curl -L https://swiftlang.github.io/swiftly/swiftly-install.sh | bash +``` + +Once swiftly is installed you can use it to install the latest available swift toolchain like this: + +``` +$ swiftly install latest + +Fetching the latest stable Swift release... +Installing Swift 5.8.1 +Downloaded 488.5 MiB of 488.5 MiB +Extracting toolchain... +Swift 5.8.1 installed successfully! + +$ swift --version + +Swift version 5.8.1 (swift-5.8.1-RELEASE) +Target: x86_64-unknown-linux-gnu +``` + +Or, you can install (and use) a swift release: + +``` +$ swiftly install --use 5.7 + +$ swift --version + +Swift version 5.7.2 (swift-5.7.2-RELEASE) +Target: x86_64-unknown-linux-gnu +``` + +There's also an option to install the latest snapshot release and get access to the latest features: + +``` +$ swiftly install main-snapshot +``` + +> Note: This last example just installed the toolchain. You can run "swiftly use" to switch to it and other installed toolchahins when you're ready. diff --git a/Documentation/SwiftlyDocs.docc/install-toolchains.md b/Documentation/SwiftlyDocs.docc/install-toolchains.md new file mode 100644 index 00000000..4f8a301f --- /dev/null +++ b/Documentation/SwiftlyDocs.docc/install-toolchains.md @@ -0,0 +1,66 @@ +# Install Swift Toolchains + +swiftly install + +Installing a swift toolchain using swiftly involves downloading it securely and extracting it into a well-known location in your account. Here we will guide you through the different ways you can install a swift toolchain. You will need to install swiftly first. The [Getting Started](getting-started.md) guide is a good place to start with swiftly. + +The easiest way to install a swift toolchain is to select the latest stable release: + +``` +$ swiftly install latest +``` + +If this is the only toolchain that is installed then swiftly will automatically "use" it so that when you run swift (or any other toolchain command) it will be this version. + +``` +$ swift --version + +Swift version 5.8.1 (swift-5.8.1-RELEASE) +Target: x86_64-unknown-linux-gnu +``` + +You can be very specific about the released version that you want. We can install the 5.6.1 version like this: + +``` +$ swiftly install 5.6.1 +``` + +Once you've installed more than one toolchain you may notice that swift is on the first version that you installed, not the last one. Swiftly lets you quickly switch between toolchains by "using" them. There's a swiftly subcommand for that. + +``` +$ swiftly use 5.6.1 +``` + +You can also combine install and use into one command to automate this process with the `--use` switch on the install subcommand: + +``` +$ swiftly install --use 5.7.1 +``` + +Sometimes you want the latest available patch of a minor release, such as 5.7. Let's omit the patch number so that we get the latest patch. + +``` +$ swiftly install 5.7 +Installing Swift 5.7.2 +``` + +Swiftly supports installing development snapshot toolchains. For example, you can install the latest available snapshot for the next major release using the "main-snapshot" selector and prepare your code for when it arrives. + +``` +$ swiftly install main-snapshot +``` + +If you are tracking down a problem on a specific snapshot you can download it using the date. + +``` +$ swiftly install main-snapshot-2022-01-28 +``` + +The same snapshot capabilities are available for version snapshots too either the latest available one, or a specific date. + +``` +$ swiftly install 5.7-snapshot +$ swiftly install 5.7-snapshot-2022-08-30 +``` + + diff --git a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md new file mode 100644 index 00000000..0d6aa98f --- /dev/null +++ b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md @@ -0,0 +1,326 @@ +# swiftly + + + +A utility for installing and managing Swift toolchains. + +``` +swiftly [--version] [--help] +``` + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + +## install + +Install a new toolchain. + +``` +swiftly install [--use] [--token=] [--verify] [--version] [--help] +``` + +**version:** + +*The version of the toolchain to install.* + + +The string "latest" can be provided to install the most recent stable version release: + + $ swiftly install latest + +A specific toolchain can be installed by providing a full toolchain name, for example a stable release version with patch (e.g. a.b.c): + + $ swiftly install 5.4.2 + +Or a snapshot with date: + + $ swiftly install 5.7-snapshot-2022-06-20 + $ swiftly install main-snapshot-2022-06-20 + +The latest patch release of a specific minor version can be installed by omitting the patch version: + + $ swiftly install 5.6 + +Likewise, the latest snapshot associated with a given development branch can be installed by omitting the date: + + $ swiftly install 5.7-snapshot + $ swiftly install main-snapshot + + +**--use:** + +*Mark the newly installed toolchain as in-use.* + + +**--token=\:** + +*A GitHub authentiation token to use for any GitHub API requests.* + + +This is useful to avoid GitHub's low rate limits. If an installation +fails with an "unauthorized" status code, it likely means the rate limit has been hit. + + +**--verify:** + +*Verify the toolchain's PGP signature before proceeding with installation.* + + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + + + +## use + +Set the active toolchain. If no toolchain is provided, print the currently in-use toolchain, if any. + +``` +swiftly use [] [--version] [--help] +``` + +**toolchain:** + +*The toolchain to use.* + + +If no toolchain is provided, the currently in-use toolchain will be printed, if any: + + $ swiftly use + +The string "latest" can be provided to use the most recent stable version release: + + $ swiftly use latest + +A specific toolchain can be selected by providing a full toolchain name, for example a stable release version with patch (e.g. a.b.c): + + $ swiftly use 5.4.2 + +Or a snapshot with date: + + $ swiftly use 5.7-snapshot-2022-06-20 + $ swiftly use main-snapshot-2022-06-20 + +The latest patch release of a specific minor version can be used by omitting the patch version: + + $ swiftly use 5.6 + +Likewise, the latest snapshot associated with a given development branch can be used by omitting the date: + + $ swiftly use 5.7-snapshot + $ swiftly use main-snapshot + + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + + + +## uninstall + +Remove an installed toolchain. + +``` +swiftly uninstall [--assume-yes] [--version] [--help] +``` + +**toolchain:** + +*The toolchain(s) to uninstall.* + + +The toolchain selector provided determines which toolchains to uninstall. Specific toolchains can be uninstalled by using their full names as the selector, for example a full stable release version with patch (a.b.c): + + $ swiftly uninstall 5.2.1 + +Or a full snapshot name with date (a.b-snapshot-YYYY-mm-dd): + + $ swiftly uninstall 5.7-snapshot-2022-06-20 + +Less specific selectors can be used to uninstall multiple toolchains at once. For instance, the patch version can be omitted to uninstall all toolchains associated with a given minor version release: + + $ swiftly uninstall 5.6 + +Similarly, all snapshot toolchains associated with a given branch can be uninstalled by omitting the date: + + $ swiftly uninstall main-snapshot + $ swiftly uninstall 5.7-snapshot + +The latest installed stable release can be uninstalled by specifying 'latest': + + $ swiftly uninstall latest + +Finally, all installed toolchains can be uninstalled by specifying 'all': + + $ swiftly uninstall all + + +**--assume-yes:** + +*Uninstall all selected toolchains without prompting for confirmation.* + + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + + + +## list + +List installed toolchains. + +``` +swiftly list [] [--version] [--help] +``` + +**toolchain-selector:** + +*A filter to use when listing toolchains.* + + +The toolchain selector determines which toolchains to list. If no selector is provided, all installed toolchains will be listed: + + $ swiftly list + +The installed toolchains associated with a given major version can be listed by specifying the major version as the selector: + + $ swiftly list 5 + +Likewise, the installed toolchains associated with a given minor version can be listed by specifying the minor version as the selector: + + $ swiftly list 5.2 + +The installed snapshots for a given devlopment branch can be listed by specifying the branch as the selector: + + $ swiftly list main-snapshot + $ swiftly list 5.7-snapshot + + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + + + +## update + +Update an installed toolchain to a newer version. + +``` +swiftly update [] [--assume-yes] [--verify] [--version] [--help] +``` + +**toolchain:** + +*The installed toolchain to update.* + + +Updating a toolchain involves uninstalling it and installing a new toolchain that is newer than it. + +If no argument is provided to the update command, the currently in-use toolchain will be updated. If that toolchain is a stable release, it will be updated to the latest patch version for that major.minor version. If the currently in-use toolchain is a snapshot, then it will be updated to the latest snapshot for that development branch. + + $ swiftly update + +The string "latest" can be provided to update the installed stable release toolchain with the newest version to the latest available stable release. This may update the toolchain to later major, minor, or patch versions. + + $ swiftly update latest + +A specific stable release can be updated to the latest patch version for that release by specifying the entire version: + + $ swiftly update 5.6.0 + +Omitting the patch in the specified version will update the latest installed toolchain for the provided minor version to the latest available release for that minor version. For example, the following will update the latest installed Swift 5.4 release toolchain to the latest available Swift 5.4 release: + + $ swiftly update 5.4 + +Similarly, omitting the minor in the specified version will update the latest installed toolchain for the provided major version to the latest available release for that major version. Note that this may update the toolchain to a later minor version. + + $ swiftly update 5 + +The latest snapshot toolchain for a given development branch can be updated to the latest available snapshot for that branch by specifying just the branch: + + $ swiftly update 5.7-snapshot + $ swiftly update main-snapshot + +A specific snapshot toolchain can be updated by including the date: + + $ swiftly update 5.9-snapshot-2023-09-20 + $ swiftly update main-snapshot-2023-09-20 + + +**--assume-yes:** + +*Update the selected toolchains without prompting for confirmation.* + + +**--verify:** + +*Verify the toolchain's PGP signature before proceeding with installation.* + + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + + + +## self-update + +Update the version of swiftly itself. + +``` +swiftly self-update [--version] [--help] +``` + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + + + diff --git a/Documentation/SwiftlyDocs.docc/uninstall-toolchains.md b/Documentation/SwiftlyDocs.docc/uninstall-toolchains.md new file mode 100644 index 00000000..2c4b4617 --- /dev/null +++ b/Documentation/SwiftlyDocs.docc/uninstall-toolchains.md @@ -0,0 +1,46 @@ +# Uninstall Swift Toolchains + +swiftly uninstall + +After installing several toolchains the list of the available toolchains to use becomes too large. Each toolchain also occupies substantial storage space. It's good to be able to cleanup toolchains when they aren't needed anymore. This guide will cover how to uninstall your toolchains assuming that you have installed swiftly and used it to install them. + +If you have a released version that you want to uninstall then give the exact three digit version (major, minor and patch): + +``` +$ swiftly uninstall 5.6.1 +``` + +When you're done working with every patch of a minor swift release you can remove them all by omiting the patch version. + +``` +$ swiftly uninstall 5.6 +``` + +Snapshots can be removed individually using the version (or main) and the date. + +``` +$ swiftly uninstall main-snapshot-2022-08-30 +$ swiftly uninstall 5.7-snapshot-2022-08-30 +``` + +It can be time consuming to remove all of the snapshots that you have installed. You can remove all of the snapshots on a version, or main with one command. + +``` +$ swiftly uninstall main-snapshot +$ swiftly uninstall 5.7-snapshot +``` + +You can see what toolchahins remain with the list subcommand like this: + +``` +$ swiftly list + +Installed release toolchains +---------------------------- +Swift 5.10.1 (in use) + +Installed snapshot toolchains +----------------------------- +``` + +Here you have seen how you can uninstall toolchains in different ways using swiftly to help manage your development environment. diff --git a/Documentation/SwiftlyDocs.docc/update-toolchain.md b/Documentation/SwiftlyDocs.docc/update-toolchain.md new file mode 100644 index 00000000..8277afca --- /dev/null +++ b/Documentation/SwiftlyDocs.docc/update-toolchain.md @@ -0,0 +1,43 @@ +# Update Swift Toolchain + +swiftly update + +Update replaces a given toolchain with a later version of that toolchain. For a stable release, this means updating to a later patch, minor, or major version. For snapshots, this means updating to the most recently available snapshot. Swiftly can help you to keep up-to-date. We assume that you have installed swiftly and use it to manage your toolchains. + +If no version is provided, update will update the currently selected toolchain to its latest patch release if a release toolchain or the latest available snapshot if a snapshot. The newly installed version will be selected. + +``` +swiftly update +``` + +To update the latest installed release version to the latest available release version, the “latest” version can be provided. Note that this may update the toolchain to the next minor or even major version. + +``` +swiftly update latest +``` + +If only a major version is specified, the latest installed toolchain with that major version will be updated to the latest available release of that major version: + +``` +swiftly update 5 +``` + +If the major and minor version are specified, the latest installed toolchain associated with that major/minor version will be updated to the latest available patch release for that major/minor version. + +``` +swiftly update 5.3 +``` + +Similarly, to update the latest snapshot associated with a specific version, the “a.b-snapshot” version can be supplied: + +``` +swiftly update 5.3-snapshot +``` + +You can also update the latest installed main snapshot to the latest available one by just providing `main-snapshot`: + +``` +swiftly update main-snapshot +``` + +Here you have seen how to use swiftly to update your toolchains to the latest, latest of a particular major/minor release, and even snapshots. diff --git a/Package.resolved b/Package.resolved index a5184e17..c95e1dd1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,4 @@ { - "originHash" : "4e45e73ff02865a9bd0426ba4c6cd8dfcb967e62e7663128dd262908109e1487", "pins" : [ { "identity" : "async-http-client", @@ -46,6 +45,24 @@ "version" : "1.1.0" } }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } + }, { "identity" : "swift-http-types", "kind" : "remoteSourceControl", @@ -137,5 +154,5 @@ } } ], - "version" : 3 + "version" : 2 } diff --git a/Package.swift b/Package.swift index 269c2a89..b03119f2 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,7 @@ let package = Package( .package(url: "https://github.com/swift-server/async-http-client", from: "1.21.2"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.64.0"), .package(url: "https://github.com/apple/swift-tools-support-core.git", from: "0.6.1"), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), ], targets: [ .executableTarget( @@ -37,6 +38,30 @@ let package = Package( .product(name: "NIOFoundationCompat", package: "swift-nio"), ] ), + .target( + name: "SwiftlyDocs", + path: "Documentation" + ), + .plugin( + name: "GenerateDocsReference", + capability: .command( + intent: .custom( + verb: "generate-docs-reference", + description: "Generate a documentation reference for swiftly." + ), + permissions: [ + .writeToPackageDirectory(reason: "This command generates documentation."), + ] + ), + dependencies: ["generate-docs-reference"] + ), + .executableTarget( + name: "generate-docs-reference", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Tools/generate-docs-reference" + ), .target( name: "LinuxPlatform", dependencies: [ diff --git a/Plugins/GenerateDocsReference/GenerateDocsReference.swift b/Plugins/GenerateDocsReference/GenerateDocsReference.swift new file mode 100644 index 00000000..d3253388 --- /dev/null +++ b/Plugins/GenerateDocsReference/GenerateDocsReference.swift @@ -0,0 +1,76 @@ +import PackagePlugin + +@main +struct GenerateDocsReferencePlugin: CommandPlugin { + func performCommand( + context: PluginContext, + arguments: [String] + ) async throws { + // Locate generation tool. + let generationToolFile = try context.tool(named: "generate-docs-reference").path + + // Create an extractor to extract plugin-only arguments from the `arguments` + // array. + var extractor = ArgumentExtractor(arguments) + + // Run generation tool once if help is requested. + if extractor.helpRequest() { + try generationToolFile.exec(arguments: ["--help"]) + print( + """ + ADDITIONAL OPTIONS: + --configuration + Tool build configuration used to generate the + reference document. (default: release) + + NOTE: The "GenerateDocsReference" plugin handles passing the "" and + "--output-directory " arguments. Manually supplying + these arguments will result in a runtime failure. + """) + return + } + + // Extract configuration argument before making it to the + // "generate-docs-reference" tool. + let configuration = try extractor.configuration() + + // Build all products first. + print("Building package in \(configuration) mode...") + let buildResult = try packageManager.build( + .product("swiftly"), + parameters: .init(configuration: configuration) + ) + + guard buildResult.succeeded else { + throw GenerateDocsReferencePluginError.buildFailed(buildResult.logText) + } + print("Built package in \(configuration) mode") + + // Run generate-docs-reference on all executable artifacts. + for builtArtifact in buildResult.builtArtifacts { + // Skip non-executable targets + guard builtArtifact.kind == .executable else { continue } + + // Get the artifacts name. + let executableName = builtArtifact.path.lastComponent + + print("Generating docs reference for \(executableName)...") + + let outputFile = context.package.directory + .appending("Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md") + + // Create generation tool arguments. + var generationToolArguments = [ + builtArtifact.path.string, + "--output-file", + outputFile.string, + ] + generationToolArguments.append( + contentsOf: extractor.remainingArguments) + + // Spawn generation tool. + try generationToolFile.exec(arguments: generationToolArguments) + print("Generated docs reference in '\(outputFile)'") + } + } +} diff --git a/Plugins/GenerateDocsReference/GenerateDocsReferenceError.swift b/Plugins/GenerateDocsReference/GenerateDocsReferenceError.swift new file mode 100644 index 00000000..fb0bd5d0 --- /dev/null +++ b/Plugins/GenerateDocsReference/GenerateDocsReferenceError.swift @@ -0,0 +1,39 @@ +import Foundation +import PackagePlugin + +enum GenerateDocsReferencePluginError: Error { + case unknownBuildConfiguration(String) + case buildFailed(String) + case createOutputDirectoryFailed(Error) + case subprocessFailedNonZeroExit(Path, Int32) + case subprocessFailedError(Path, Error) +} + +extension GenerateDocsReferencePluginError: CustomStringConvertible { + var description: String { + switch self { + case let .unknownBuildConfiguration(configuration): + return "Build failed: Unknown build configuration '\(configuration)'." + case let .buildFailed(logText): + return "Build failed: \(logText)." + case let .createOutputDirectoryFailed(error): + return """ + Failed to create output directory: '\(error.localizedDescription)' + """ + case let .subprocessFailedNonZeroExit(tool, exitCode): + return """ + '\(tool.lastComponent)' invocation failed with a nonzero exit code: \ + '\(exitCode)'. + """ + case let .subprocessFailedError(tool, error): + return """ + '\(tool.lastComponent)' invocation failed: \ + '\(error.localizedDescription)' + """ + } + } +} + +extension GenerateDocsReferencePluginError: LocalizedError { + var errorDescription: String? { self.description } +} diff --git a/Plugins/GenerateDocsReference/PackagePlugin+Helpers.swift b/Plugins/GenerateDocsReference/PackagePlugin+Helpers.swift new file mode 100644 index 00000000..53f08344 --- /dev/null +++ b/Plugins/GenerateDocsReference/PackagePlugin+Helpers.swift @@ -0,0 +1,85 @@ +import Foundation +import PackagePlugin + +extension ArgumentExtractor { + mutating func helpRequest() -> Bool { + self.extractFlag(named: "help") > 0 + } + + mutating func configuration() throws -> PackageManager.BuildConfiguration { + switch self.extractOption(named: "configuration").first { + case let .some(configurationString): + switch configurationString { + case "debug": + return .debug + case "release": + return .release + default: + throw + GenerateDocsReferencePluginError + .unknownBuildConfiguration(configurationString) + } + case .none: + return .release + } + } +} + +extension Path { + func createOutputDirectory() throws { + do { + try FileManager.default.createDirectory( + atPath: self.string, + withIntermediateDirectories: true + ) + } catch { + throw GenerateDocsReferencePluginError.createOutputDirectoryFailed(error) + } + } + + func exec(arguments: [String]) throws { + do { + let process = Process() + process.executableURL = URL(fileURLWithPath: self.string) + process.arguments = arguments + try process.run() + process.waitUntilExit() + guard + process.terminationReason == .exit, + process.terminationStatus == 0 + else { + throw GenerateDocsReferencePluginError.subprocessFailedNonZeroExit( + self, process.terminationStatus + ) + } + } catch { + throw GenerateDocsReferencePluginError.subprocessFailedError(self, error) + } + } +} + +extension PackageManager.BuildResult.BuiltArtifact { + func matchingProduct(context: PluginContext) -> Product? { + context + .package + .products + .first { $0.name == self.path.lastComponent } + } +} + +extension Product { + func hasDependency(named name: String) -> Bool { + self.recursiveTargetDependencies + .contains { $0.name == name } + } + + var recursiveTargetDependencies: [Target] { + var dependencies = [Target.ID: Target]() + for target in self.targets { + for dependency in target.recursiveTargetDependencies { + dependencies[dependency.id] = dependency + } + } + return Array(dependencies.values) + } +} diff --git a/RELEASING.md b/RELEASING.md index 5f5e6348..36895b40 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -6,15 +6,19 @@ Swiftly and the swiftly-install release script have different release schedules 1. Check out the commit you wish to create a release for. Ensure no other local modifications or changes are present. -2. Ensure the version string in `SwiftlyCore/SwiftlyCore.swift` is accurate. If it is not, push another commit updating it to the proper value. +2. Check the validity of the documentation preview with `swift package --disable-sandbox preview-documentation --target SwiftlyDocs` -3. Create a tag on that commit with the format "x.y.z". Do not omit "z", even if its value is 0. +3. Verify that the swiftly command-line reference is up-to-date, if not then run `swift package plugin generate-docs-reference` to update it. -4. Build the executables for the release by running ./scripts/build_release.sh from the root of the swiftly repository (do this once on an x86_64 machine and once on an aarch64 one) +4. Ensure the version string in `SwiftlyCore/SwiftlyCore.swift` is accurate. If it is not, push another commit updating it to the proper value. -5. Push the tag to `origin`. `git push origin ` +5. Create a tag on that commit with the format "x.y.z". Do not omit "z", even if its value is 0. -6. Go to the GitHub page for the new tag, click edit tag, add an appropriate description, attach the prebuilt executables, and click "Publish Release". +6. Build the executables for the release by running ./scripts/build_release.sh from the root of the swiftly repository (do this once on an x86_64 machine and once on an aarch64 one) + +7. Push the tag to `origin`. `git push origin ` + +8. Go to the GitHub page for the new tag, click edit tag, add an appropriate description, attach the prebuilt executables, and click "Publish Release". ## Releasing swiftly-install diff --git a/Tools/generate-docs-reference/Extensions/Process+SimpleAPI.swift b/Tools/generate-docs-reference/Extensions/Process+SimpleAPI.swift new file mode 100644 index 00000000..7202af36 --- /dev/null +++ b/Tools/generate-docs-reference/Extensions/Process+SimpleAPI.swift @@ -0,0 +1,64 @@ +import Foundation + +enum SubprocessError: Swift.Error, LocalizedError, CustomStringConvertible { + case missingExecutable(url: URL) + case failedToLaunch(error: Swift.Error) + case nonZeroExitCode(code: Int) + + var description: String { + switch self { + case let .missingExecutable(url): + return "No executable at '\(url.standardizedFileURL.path)'." + case let .failedToLaunch(error): + return "Couldn't run command process. \(error.localizedDescription)" + case let .nonZeroExitCode(code): + return "Process returned non-zero exit code '\(code)'." + } + } + + var errorDescription: String? { self.description } +} + +func executeCommand( + executable: URL, + arguments: [String] +) throws -> String { + guard (try? executable.checkResourceIsReachable()) ?? false else { + throw SubprocessError.missingExecutable(url: executable) + } + + let process = Process() + if #available(macOS 10.13, *) { + process.executableURL = executable + } else { + process.launchPath = executable.path + } + process.arguments = arguments + + let output = Pipe() + process.standardOutput = output + process.standardError = FileHandle.nullDevice + + if #available(macOS 10.13, *) { + do { + try process.run() + } catch { + throw SubprocessError.failedToLaunch(error: error) + } + } else { + process.launch() + } + let outputData = output.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + + guard process.terminationStatus == 0 else { + throw SubprocessError.nonZeroExitCode(code: Int(process.terminationStatus)) + } + + let outputActual = + String(data: outputData, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) + ?? "" + + return outputActual +} diff --git a/Tools/generate-docs-reference/GenerateDocsReference.swift b/Tools/generate-docs-reference/GenerateDocsReference.swift new file mode 100644 index 00000000..77cc2cde --- /dev/null +++ b/Tools/generate-docs-reference/GenerateDocsReference.swift @@ -0,0 +1,203 @@ +import ArgumentParser +import ArgumentParserToolInfo +import Foundation + +@main +struct GenerateDocsReferencel: ParsableCommand { + enum Error: Swift.Error { + case failedToRunSubprocess(error: Swift.Error) + case unableToParseToolOutput(error: Swift.Error) + case unsupportedDumpHelpVersion(expected: Int, found: Int) + case failedToGenerateDocsReferencePage(error: Swift.Error) + } + + static let configuration = CommandConfiguration( + commandName: "generate-docs-reference", + abstract: "Generate a docs reference for the provided tool." + ) + + @Argument(help: "Tool to generate docs.") + var tool: String + + @Option(name: .shortAndLong, help: "File to save generated docs. Use '-' for stdout.") + var outputFile: String + + func validate() throws {} + + func run() throws { + let data: Data + do { + let tool = URL(fileURLWithPath: tool) + let output = try executeCommand( + executable: tool, arguments: ["--experimental-dump-help"] + ) + data = output.data(using: .utf8) ?? Data() + } catch { + throw Error.failedToRunSubprocess(error: error) + } + + do { + let toolInfoThin = try JSONDecoder().decode(ToolInfoHeader.self, from: data) + guard toolInfoThin.serializationVersion == 0 else { + throw Error.unsupportedDumpHelpVersion( + expected: 0, + found: toolInfoThin.serializationVersion + ) + } + } catch { + throw Error.unableToParseToolOutput(error: error) + } + + let toolInfo: ToolInfoV0 + do { + toolInfo = try JSONDecoder().decode(ToolInfoV0.self, from: data) + } catch { + throw Error.unableToParseToolOutput(error: error) + } + + do { + if self.outputFile == "-" { + try self.generatePages(from: toolInfo.command, savingTo: nil) + } else { + try self.generatePages( + from: toolInfo.command, + savingTo: URL(fileURLWithPath: self.outputFile) + ) + } + } catch { + throw Error.failedToGenerateDocsReferencePage(error: error) + } + } + + func generatePages(from command: CommandInfoV0, savingTo file: URL?) throws { + let page = command.toMD([]) + + if let file { + try page.write(to: file, atomically: true, encoding: .utf8) + } else { + print(page) + } + } +} + +extension CommandInfoV0 { + public func toMD(_ path: [String]) -> String { + var result = String(repeating: "#", count: path.count + 1) + " \(self.commandName)\n\n" + + if path.count == 0 { + result += + "\n\n" + } + + if let abstract = self.abstract { + result += "\(abstract)\n\n" + } + + if let args = self.arguments, args.count != 0 { + result += "```\n" + result += (path + [self.commandName]).joined(separator: " ") + " " + self.usage() + result += "\n```\n\n" + } + + if let discussion = self.discussion { + result += "\(discussion)\n\n" + } + + if let args = self.arguments { + for arg in args { + guard arg.shouldDisplay else { + continue + } + + result += "**\(arg.identity()):**\n\n" + if let abstract = arg.abstract { + result += "*\(abstract)*\n\n" + } + if let discussion = arg.discussion { + result += discussion + "\n\n" + } + result += "\n" + } + } + + for subcommand in self.subcommands ?? [] { + result += subcommand.toMD(path + [self.commandName]) + "\n\n" + } + + return result + } + + public func usage() -> String { + guard let args = self.arguments else { + return "" + } + + return args.map { $0.usage() }.joined(separator: " ") + } +} + +extension ArgumentInfoV0 { + public func usage() -> String { + guard self.shouldDisplay else { + return "" + } + + let name: String + if let preferred = self.preferredName { + name = preferred.name + } else if let value = self.valueName { + name = value + } else { + return "" + } + + // TODO: default values, short, etc. + + var inner = + switch self.kind + { + case .positional: + "<\(name)>" + case .option: + "--\(name)=<\(self.valueName ?? "")>" + case .flag: + "--\(name)" + } + + if self.isRepeating { + inner += "..." + } + + if self.isOptional { + return "[\(inner)]" + } + + return inner + } + + public func identity() -> String { + let name: String + if let preferred = self.preferredName { + name = preferred.name + } else if let value = self.valueName { + name = value + } else { + return "" + } + + // TODO: default values, values, short, etc. + + let inner = + switch self.kind + { + case .positional: + "\(name)" + case .option: + "--\(name)=\\<\(self.valueName ?? "")\\>" + case .flag: + "--\(name)" + } + + return inner + } +}