-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add PackageExtension library and incorporate build tool commands into native build system #3287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add PackageExtension library and incorporate build tool commands into native build system #3287
Conversation
@@ -10,11 +10,23 @@ let package = Package( | |||
// target: "MySourceGenExt" | |||
// ) | |||
], | |||
dependencies: [ | |||
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "0.3.1")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot that I still have an external package reference here. I wanted an example that uses SwiftArgumentParser, which a real tool would, but will change this test fixture so that no network access is needed while running unit tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe use a local dependency like "../Foo"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what we usually do but I didn't want to vendor all of SwiftArgParser. In this case it isn't actually to test that we can use the dependency (that will come in a later fixture, with an extension in a depenency package), but this was just to have it be "real" since pretty much every command line tool source generator written in Swift will probably use ArgParser.
So I'll remove this one though. but ../Foo
is a good idea for the upcoming extension-in-a-dependency fixture.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another option is to have this Fixture run only when testing locally so CI does not do network access
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. How'd you normally do that — environment variables?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea that's one way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. In this case it's not essential for this test, so there's no point spending the cycles even on local tests. I will modify the test case.
|
||
extension String { | ||
|
||
public var quotedForSourceCode: String { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just to show the ability for the tool that's built for use by the extension to depend on its own library in turn.
@@ -30,6 +30,9 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS | |||
|
|||
/// The closure for loading the package graph. | |||
let packageGraphLoader: () throws -> PackageGraph | |||
|
|||
/// The closure for evaluating extensions in the package graph. | |||
let extensionEvaluator: (PackageGraph) throws -> [ResolvedTarget: [ExtensionEvaluationResult]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't an ideal way to call this, but is very much along the lines of the existing packageGraphLoader
. The entire BuildOperation layer could use some refactoring.
for command in extensionEvaluationResults.reduce([], { $0 + $1.commands }) { | ||
// Prebuild and postbuild commands are handled outside the build system. | ||
if case .buildToolCommand(_, _, _, _, _, _, _, let derivedSourcePaths) = command { | ||
// TODO: What should we do if we find non-Swift sources here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are still several diagnostics to add of this nature — it should be an error, earlier on, if an extension emits source code that isn't compatible with the target into which it's being compiled. This is because SwiftPM doesn't yet allow mixed sources, and it's beyond the scope of the extensibility proposal to change that. In this case, this error should be caught and reported earlier on.
@@ -510,6 +510,8 @@ extension LLBuildManifestBuilder { | |||
if target.underlyingTarget is SystemLibraryTarget { return } | |||
// Ignore Binary Modules. | |||
if target.underlyingTarget is BinaryTarget { return } | |||
// Ignore Extension Targets. | |||
if target.underlyingTarget is ExtensionTarget { return } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we don't reflect the extension target's dependencies here is because we will later set up build rules that establish that establishes that dependency (just below).
do { | ||
// Configure the inputs to the extension evaluation. | ||
// FIXME: These paths are still fairly preliminary. | ||
let buildEnvironment = try buildParameters().buildEnvironment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part of the code will evolve in future PRs. There are likely to be edge cases that need to be fixed before the feature can be enabled after approval.
@@ -594,6 +594,33 @@ public class SwiftTool { | |||
throw error | |||
} | |||
} | |||
|
|||
/// Evaluate extensions for any reachable targets in the graph, and return a mapping from targets to corresponding evaluation results. | |||
func evaluateExtensions(graph: PackageGraph) throws -> [ResolvedTarget: [ExtensionEvaluationResult]] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every build system (of any IDE) that wants to incorporate libSwiftPM support would need to do something like this, where after loading the package graph, it evaluates the package extensions in the graph and gets back a mapping from targets to the ordered list of results of applying the target's specified extensions to that target. Those results are build-system neutral descriptions of command that then need to be incorporated into the build system as appropriate.
diagnostics: diagnostics, | ||
stdoutStream: stdoutStream | ||
) | ||
case .xcode: | ||
let graphLoader = { try self.loadPackageGraph(explicitProduct: explicitProduct, createMultipleTestProducts: true) } | ||
// FIXME: Implement the custom build command provider also. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Support for the XCBuild build system will follow in a future PR.
# See http://swift.org/LICENSE.txt for license information | ||
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors | ||
|
||
add_library(PackageExtension |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This glue is all fairly similar to the corresponding logic for PackageDescription, except that it doesn't have to build two different versions.
public func lookupTool(named name: String) throws -> Path { | ||
// TODO: Rather than just appending the name, this should instead use | ||
// a mapping of tool names to paths (passed in from the context). | ||
return execsDir.appending(name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is preliminary. See the comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right. makes sense for Context to take in a function that will do the lookup, or a name:path map
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely. In particular, it should be an error to depend on anything you didn't declare a dependency on.
My only concern is that this will preclude quick workarounds, such as dropping a tool into /usr/local/bin etc. Perhaps that should be a warning if you use a tool you didn't depend on.
One could also argue that a standard set of tools in the toolchain should be fair game, e.g. strip
etc. Certainly awk
and others seem reasonable. But it's something to be discussed, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With all the things in this PR, I'm trying to stub out things that can be decided later if we think that it will "slot into" the current approach in a reasonable way. This is one of those where I think a name-to-path map would be reasonable.
public let moduleName: String | ||
|
||
/// The path of the target directory. | ||
public let targetDir: Path |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will likely want to expose public-headers directories etc, but that will be easy to thread through.
/// is in topologically sorted order, with immediate dependencies appearing | ||
/// earlier and more distant dependencies later in the list. This is mainly | ||
/// intended for generating lists of search path arguments, etc. | ||
public let dependencies: [DependencyTargetInfo] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only some tools, such as protoc
, need to see information about the dependency closure.
|
||
/// Absolute paths of the source files in the target. This might include | ||
/// derived source files generated by other extensions). | ||
public let sourceFiles: [Path] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are likely to want to refine the notion of source vs resource vs other files before enabling this. Currently SwiftPM makes this determination before extensions have a chance to run — it would clearly be more flexible to have the extensions be a part of this determination. This will be refined in future PRs.
atexit { | ||
let encoder = JSONEncoder() | ||
let data = try! encoder.encode(output) | ||
fputc(0, stdout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is explained more thoroughly at the top of this source file. The way in which libPackageExtension communicates its information back to SwiftPM is a private implementation detail, but the idea is to avoid reliance on a Unix pipe (which is not as platform-neutral as one would like) or a separate file (which will require write access in the file system) and similar side-channel solutions by reusing stdout and simply considering the structured information to be the bytes after the last null byte.
fflush(stdout) | ||
} | ||
// Look for the input JSON as the last argument of the invocation. | ||
guard let data = ProcessInfo().arguments.last?.data(using: .utf8) else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is something we want to fix. It would probably be better to pass it in on stdin (to avoid any limits on command line lengths), but we don't have TSC support for that yet.
// the JSON-serialized ExtensionEvaluationResult. Since this appears after any free-form output from the | ||
// script, it can be safely split out while maintaining the ability to see debug output without resorting | ||
// to side-channel communication that might be not be very cross-platform (e.g. pipes, file handles, etc). | ||
var stdoutPieces = try result.output.get().split(separator: 0, omittingEmptySubsequences: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See earlier comments.
fileprivate func invoke(compiledExec: AbsolutePath, input: Data) throws -> (outputJSON: Data, stdoutText: Data) { | ||
// FIXME: It would be more robust to pass it as `stdin` data, but we need TSC support for that. When this is | ||
// changed, PackageExtension will need to change as well (but no extensions need to change). | ||
var command = [compiledExec.pathString] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Support for running this in a sandbox is coming in a future PR.
@swift-ci please smoke test |
Investigating the Linux failure. I expect that this will take a couple of tries to get right, what with the new PackageExtension library, etc. |
let diagnostics = DiagnosticsEngine() | ||
|
||
// Create the cache directory, if needed. | ||
try localFileSystem.createDirectory(cacheDir, recursive: true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we also need to create the outputs dir?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Technically we don't have to create either of them, since that happens lower down in the stack. We can probably just make it part of the contract that evaluateExtensions()
creates the directories as needed, so that not every client has to. I'll fix it. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this call as redundant.
@swift-ci please smoke test |
} | ||
|
||
/// A constructed command. | ||
fileprivate struct Command: Encodable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
side question: why do all these need to be decodable / coddle?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are passed back to SwiftPM via JSON. That's also how the context comes in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part is just like PackageDescription. But it's not visible in the public-facing API that that's what happens.
|
||
public func fatalError(_ message: String, file: Path? = nil, line: Int? = nil) { | ||
emit(error: message, file: file, line: line) | ||
exit(1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It needs a better implementation, but I expect that unlike the package description, there will need to be a way to do error checking on things like not being able to find a tool and then good error messages. It could also be a thrown error though. This is something I'd like to discuss — we didn't talk that much about what a good error API for this use case should look like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this for now. We can discuss the best API here as a refinement.
Hmm... the macOS error is due to a source error that I'm not seeing locally on macOS. I'd be curious to know what's different about the tools, but can certainly change the code to make local and bot both be happy. |
Interesting, I'm using 5.4 locally, and the macOS tools are
I'll change the code so 5.3 is happy with it. Linux toolchain is:
which explains why it succeeded. |
@swift-ci please smoke test |
I'll fix the macOS source code failure (seems specific to 5.3 toolchain — Linux is using 5.4 dev and works fine) in about an hour. |
0d1b747
to
9a52682
Compare
@swift-ci please smoke test |
1 similar comment
@swift-ci please smoke test |
Sources/Build/BuildPlan.swift
Outdated
@@ -544,11 +544,15 @@ public final class SwiftTargetBuildDescription { | |||
|
|||
/// The modulemap file for this target, if any. | |||
private(set) var moduleMap: AbsolutePath? | |||
|
|||
/// The results of having applied any extensions to this target. | |||
public private(set) var extensionEvaluationResults: [ExtensionEvaluationResult] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this needs to be a "var" if it is set on the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point — that can be a let. I modelled it after the others but there's no need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have made this fix.
Sources/Build/BuildPlan.swift
Outdated
@@ -1259,6 +1278,9 @@ public class BuildPlan { | |||
return AnySequence(productMap.values) | |||
} | |||
|
|||
/// The results of evaluating any extensions used by targets in this build. | |||
private(set) var extensionEvaluationResults: [ResolvedTarget: [ExtensionEvaluationResult]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've made this fix as well.
} | ||
|
||
// FIXME: This is preliminary. | ||
public struct Path: ExpressibleByStringLiteral, Encodable, Decodable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is unfortunate we need to define such in SwiftPM. would adding a dependency on https://github.com/apple/swift-system help?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is really unfortunate. The issue is that the PackageExtension API has to be self-contained, like PackageDescription. If SwiftSystem becomes like Foundation or StdLib so that in be just imported (I don't know if that's the plan) then we could rely on it. But we should at least make sure the API is the same. Would it be awful to embed the same implementation?
|
||
/// Helper function that compiles an extension as an executable and returns the path to it. | ||
fileprivate func compile(sources: Sources, toolsVersion: ToolsVersion, cacheDir: AbsolutePath) throws -> AbsolutePath { | ||
// FIXME: Much of this is copied from the ManifestLoader and should be consolidated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coud we move this to some utility helper that can be reused form both places? seem unfortunate to have this twice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, absolutely. That's the consolidation work I mentioned, where this would be combined with that in ManifestLoader to have a single API for running scripts that are similar to PackageDescription or PackageExtension. I'll make sure this is covered by a Radar and reference it here.
|
||
|
||
/// An extension runner that compiles the extension as an executable binary for the host platform, and invokes it as a subprocess. | ||
public struct DefaultExtensionRunner: ExtensionRunner { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have missed it, but I did not see any sandboxing here. is that still pending?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
saw the comment about this coming in following PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this will need to be added to both the running of this script and the command that gets created.
import PackageModel | ||
import SPMBuildCore | ||
import Foundation | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be good to run formatter on all the new files (like this one) so at least new additions are formatted in a standard way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do. That's swift-lint
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
brew install swift-format
swift-format <file or directory>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, will do that for the new files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've reformatted the new ones (though using brew install swiftformat
, not swift-format
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a large PR so hard to review effectively. overall seems reasonable, I have some questions about sandboxing - which may have been left for future PR intentionally
Thanks for reviewing! I will apply the comments here, and yes, there are currently several things left for future PRs. I can iterate on this one or open separate ones. I did split this into multiple commits to make things easier to look at, but can also split them into separate PRs. |
…sources, and extend the ExtensionEvaluator to pass them on to extensions.
…sion, which will control availability annotations in the PackageExtension module.
…ption). This contains the APIs that extensions can use.
…native build system (for build tools only so far; prebuild and postbuild are coming). The default extension runner compiles and runs the extension as an executable, passing it input and receiving output in coordination with the implementation in PackageExtension (this works much the same as PackageDescription). There are still some stubbed-out areas to be filled in, and there needs to be caching and sandboxing added, but it is complete and works for what is implemented so far.
…PackageDescription, which needs either `4` or `4_2` appended to the path, there is no need for that for PackageExtension.
…though it's a small special-purpose library, there is no reason to keep it all in one source files. Internal declarations will work instead of fileprivates.
…t-parser) while running.
5fa12e0
to
6574d35
Compare
@swift-ci please smoke test |
@swift-ci please smoke test |
Add the new PackageExtension module and library and the other necessary pieces to invoke build tool commands from the native build system. This is only accessed when the feature flag to enable the not-yet-approved extensibility feature is set.
Motivation:
This is continued work toward the draft extensibility proposal. All of this functionality is still gated by feature flags that allows or prevents extension targets from appearing in the package graph, and it's likely that further changes will be made before the proposal is accepted. But this way there will be a complete implementation to try at the start of the proposal review period.
Modifications:
There are several TODOs and FIXMEs in the code. They will be addressed in future PRs.
This builds on #3286 and it is best to review that PR first and then just the separate diffs that make up this PR.