-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Bring back the implementation of SE-0301 #5802
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
Conversation
@swift-ci please smoke test |
Looks like there's an issue with compiling with 5.6 again 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.
Thanks for bringing this up-to-date! I left some comments below regarding a few parts which could be modernized further. Also, do you mind adding a Co-authored-by: Owen Voorhees <[email protected]>
entry to the commit?
@@ -0,0 +1,210 @@ | |||
/* |
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.
Some of this file could be folded into SwiftSyntax's new SwiftBasicFormat module, although there are some assumptions here which are more likely to hold in a manifest compared to arbitrary swift sources
public init(_ manifest: String, diagnosticsEngine: DiagnosticsEngine) throws { | ||
self.originalManifest = manifest | ||
self.diagnosticsEngine = diagnosticsEngine | ||
self.editedSource = try Parser.parse(source: manifest) |
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 believe this method no longer throws
name: TokenSyntax.identifier("product"), | ||
declNameArguments: nil) | ||
|
||
let argumentList = TupleExprElementListSyntax([ |
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 and a few other constructions could be simplified a lot by taking advantage of swiftlang/swift-syntax#848
@@ -0,0 +1,482 @@ | |||
/* |
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 pared-down version of what's in SPMTestSupport, it should be possible to remove it entirely
@@ -0,0 +1,67 @@ | |||
/* |
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.
Most of this can likely be replaced by existing utilities in SPMTestSupport too
@@ -385,6 +396,12 @@ let package = Package( | |||
dependencies: ["Commands"], | |||
exclude: ["CMakeLists.txt"] | |||
), | |||
.executableTarget( |
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.
PackageSyntax links enough of SwiftPM that I expect this will add 20-30mb to the toolchain size. It would be great if we could start dynamically linking some of the core libraries
This is basically bringing back @owenv's work from https://github.com/owenv/swift-package-editor into SwiftPM itself. Previously, we didn't have a way to use swift-syntax from the toolchain, but with the new `SwiftParser` library, it has now become independent of the toolchain and that problem has been solved. I talked to @owenv, updated his code/tests for the newer APIs in swift-syntax and SwiftPM. Right now, I have not brought over the integration tests, yet, only the unit tests. I will also have to do some updates to support newer manifests versions, e.g. by adding a way to use registry dependencies. In contrast to the original proposal, I'm keeping this in a separate top-level command called `swift package-editor`. This is up for discussion, but I think it would be good to start moduralizing the CI a bit more to avoid having to build every dependency and module in the first CMake stage. That would require taking aprt the `Commands` module eventually, for now I am just opting to not add more functionality into that since it just makes everything very monolithic. Co-authored-by: Owen Voorhees <[email protected]>
Ah nice, I didn't know about |
@@ -586,6 +607,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { | |||
.package(url: "https://github.com/apple/swift-crypto.git", .upToNextMinor(from: minimumCryptoVersion)), | |||
.package(url: "https://github.com/apple/swift-system.git", .upToNextMinor(from: "1.1.1")), | |||
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMinor(from: "1.0.1")), | |||
.package(url: "https://github.com/apple/swift-syntax.git", .branch(relatedDependenciesBranch)), |
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.
can we sort these alphabetically?
import SwiftSyntax | ||
|
||
extension ArrayExprSyntax { | ||
public func withAdditionalElementExpr(_ expr: ExprSyntax) -> ArrayExprSyntax { |
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.
lets audit what must be public and what can remain internal / private in order to control the public API surface area
public var lineUnitIndent: Trivia { | ||
if lineIndent.allSatisfy(\.isSpaces) { | ||
let addedSpaces = lineIndent.reduce(0, { | ||
guard case .spaces(let count) = $1 else { fatalError() } |
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.
remove use of fatalError, force unwraps, etc
branchAndRevisionConvenienceMethodsSupported: branchAndRevisionConvenienceMethodsSupported | ||
).visit(packageDependencies).root | ||
|
||
self.editedSource = newManifest.as(SourceFileSyntax.self)! |
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.
remove use of fatalError, force unwraps, etc
let packageDependenciesFinder = ArrayExprArgumentFinder(expectedLabel: "dependencies") | ||
packageDependenciesFinder.walk(argListWithDependencies) | ||
guard case .found(let newPackageDependencies) = packageDependenciesFinder.result else { | ||
fatalError("Could not find just inserted dependencies array") |
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.
remove use of fatalError, force unwraps, etc
packageDependencies = newPackageDependencies | ||
case .incompatibleExpr: | ||
diagnosticsEngine.emit(.incompatibleArgument(name: "targets")) | ||
throw Diagnostics.fatalError |
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.
should this throw instead of editing diagnostics then throwing generic diagnostics error?
|
||
// Add the target dependency entry. | ||
let newManifest = targetDependencies.withAdditionalElementExpr(ExprSyntax(callExpr)).root | ||
self.editedSource = newManifest.as(SourceFileSyntax.self)! |
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.
remove use of fatalError, force unwraps, etc
|
||
guard case .found(let targetDependencies) = targetDependencyFinder.result else { | ||
diagnosticsEngine.emit(.missingArgument(name: "dependencies", parent: "target '\(target)'")) | ||
throw Diagnostics.fatalError |
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
targetFinder.walk(targetsArrayExpr) | ||
guard let targetNode = targetFinder.foundEntity else { | ||
diagnosticsEngine.emit(.missingTarget(name: target)) | ||
throw Diagnostics.fatalError |
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
targetsArrayFinder.walk(initFnExpr.argumentList) | ||
guard case .found(let targetsArrayExpr) = targetsArrayFinder.result else { | ||
diagnosticsEngine.emit(.missingPackageInitArgument(name: "targets")) | ||
throw Diagnostics.fatalError |
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.
should this throw instead of editing diagnostics then throwing generic diagnostics error?
if Foundation.URL(string: urlOrPath)?.scheme == nil { | ||
guard checksum == nil else { | ||
diagnosticsEngine.emit(.unexpectedChecksumForBinaryTarget(path: urlOrPath)) | ||
throw Diagnostics.fatalError |
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
let productsFinder = ArrayExprArgumentFinder(expectedLabel: "products") | ||
productsFinder.walk(argListWithProducts) | ||
guard case .found(let newProducts) = productsFinder.result else { | ||
fatalError("Could not find just inserted products array") |
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
productsNode = newProducts | ||
case .incompatibleExpr: | ||
diagnosticsEngine.emit(.incompatibleArgument(name: "products")) | ||
throw Diagnostics.fatalError |
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
productsArrayFinder.walk(initFnExpr.argumentList) | ||
guard case .found(let productsArrayExpr) = productsArrayFinder.result else { | ||
diagnosticsEngine.emit(.missingPackageInitArgument(name: "products")) | ||
throw Diagnostics.fatalError |
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
productFinder.walk(productsArrayExpr) | ||
guard let productNode = productFinder.foundEntity else { | ||
diagnosticsEngine.emit(.missingProduct(name: product)) | ||
throw Diagnostics.fatalError |
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
|
||
guard case .found(let productTargets) = productTargetsFinder.result else { | ||
diagnosticsEngine.emit(.missingArgument(name: "targets", parent: "product '\(product)'")) | ||
throw Diagnostics.fatalError |
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
throw Diagnostics.fatalError | ||
case .missing: | ||
diagnosticsEngine.emit(.missingPackageInit) | ||
throw Diagnostics.fatalError |
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
return initFnExpr | ||
case .foundMultiple: | ||
diagnosticsEngine.emit(.multiplePackageInits) | ||
throw Diagnostics.fatalError |
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
targetsNode = newTargets | ||
case .incompatibleExpr: | ||
diagnosticsEngine.emit(.incompatibleArgument(name: "targets")) | ||
throw Diagnostics.fatalError |
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
let targetsFinder = ArrayExprArgumentFinder(expectedLabel: "targets") | ||
targetsFinder.walk(argListWithTargets) | ||
guard case .found(let newTargets) = targetsFinder.result else { | ||
fatalError("Could not find just-inserted targets array") |
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
|
||
do { | ||
try editorTool.packageEditor.addPackageDependency(url: dependencyURL, requirement: requirement) | ||
} catch Diagnostics.fatalError { |
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 think we want to get away from Diagnostics.fatalError, its still in place for partial manifest loading but in this case it seems like we can just throw at the call site instead of producing diagnostics then throwing Diagnostics.fatalError
do { | ||
let mapping = try createProductPackageNameMapping(packageGraph: editorTool.loadPackageGraph()) | ||
try editorTool.packageEditor.addTarget(newTarget, productPackageNameMapping: mapping) | ||
} catch Diagnostics.fatalError { |
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
let repositoryManager: RepositoryManager | ||
|
||
/// The file system in use. | ||
let fs: FileSystem |
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.
rename fs -> fileSystem (everywhere)
} | ||
|
||
/// The global context for package editor. | ||
public final class PackageEditorContext { |
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.
audit use of class, we should prefer structs unless we need a reference type
public init(manifestPath: AbsolutePath, | ||
repositoryManager: RepositoryManager, | ||
toolchain: UserToolchain, | ||
diagnosticsEngine: DiagnosticsEngine, |
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.
DiagnosticsEngine 😱
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.
feels like this initializer should be simplified after the changes we done to the workspace initializers
repositoryManager: RepositoryManager, | ||
toolchain: UserToolchain, | ||
diagnosticsEngine: DiagnosticsEngine, | ||
fs: FileSystem = localFileSystem) throws { |
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 not default to localFileSystem. you can create an extension with a simper signatures (ie defaults) for testing if that is why we are defaulting
self.diagnosticsEngine = diagnosticsEngine | ||
self.fs = fs | ||
|
||
self.manifestLoader = ManifestLoader(toolchain: toolchain) |
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.
probably use the same one we use for the workspace. see comment above about improving this initializer
try withTemporaryDirectory { | ||
let path = $0 | ||
try localFileSystem.writeFileContents(path.appending(component: "Package.swift"), | ||
bytes: ByteString(encodingAsUTF8: contents)) |
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.
no need to use ByteString
let path = $0 | ||
try localFileSystem.writeFileContents(path.appending(component: "Package.swift"), | ||
bytes: ByteString(encodingAsUTF8: contents)) | ||
_ = try loadManifest(at: path, fs: localFileSystem) |
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.
localFileSystem
} | ||
|
||
/// Load the manifest at the given path. | ||
func loadManifest( |
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.
use workspace APIs?
private var cachedPackageGraph: PackageGraph? | ||
|
||
init() throws { | ||
diagnostics = DiagnosticsEngine(handlers: [print(diagnostic:)]) |
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.
😱
if let cachedPackageGraph = cachedPackageGraph { | ||
return cachedPackageGraph | ||
} | ||
let workspace = try Workspace.init(forRootPackage: packageRoot, customManifestLoader: nil) |
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.
adjust to new APIs
return cachedPackageGraph | ||
} | ||
let workspace = try Workspace.init(forRootPackage: packageRoot, customManifestLoader: nil) | ||
let observability = ObservabilitySystem { _, _ 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.
use the one we use in other tools
|
||
func loadPackageGraph() throws -> PackageGraph { | ||
if let cachedPackageGraph = cachedPackageGraph { | ||
return cachedPackageGraph |
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.
is this safe to cache?
let repositoryManager = RepositoryManager(fileSystem: localFileSystem, | ||
path: packageRoot.appending(component: ".build"), | ||
provider: GitRepositoryProvider(), | ||
initializationWarningHandler: { _ 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.
feels like this setup sequence could / should be refactored to lean more on code from other tools / SwiftTool. it glosses over some details that may cause issues
public init?(argument: String) { | ||
self.init(argument) | ||
} | ||
} |
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 these need to be public? they will leak if so
} | ||
} | ||
|
||
func print(diagnostic: TSCBasic.Diagnostic) { |
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.
seems redundant, switch to Observability system instead of roll your own
} | ||
} | ||
|
||
func XCTAssertThrows<T: Swift.Error>( |
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.
XCTest already has something similar iirc?
} | ||
|
||
extension FileSystem { | ||
func createEmptyFiles(at root: AbsolutePath, files: 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.
FileSystem already has similar API iirc?
} | ||
#endif | ||
|
||
public class Resources { |
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 need to be public
this is pretty awesome @neonichu @owenv, very happy to see this make a come back ❤️ 🙇 did a first cut and added comment mostly around moving to the new APIs and making code safe / less fragile. the code that handles dependencies is a bit lacking in correctness afaict, we need to find a way to consolidate this with the already existing logic in the manifest parser itself. maybe there is a way to abstract some things out so its reusable in both places did not review any of the parsing logic, I think this a job better suited to @WowbaggersLiquidLunch :D |
I think we have all the pieces in place now to be able to build a command without building it during bootstrap. Are we OK with this being cc @tomerd |
Curious, is there a proposed plan for getting this merged? @tomerd @neonichu I was reviewing the updated swift.org website and really enjoyed the new layout and walkthroughs to get people up and running with swift. When scrolling through the CLI tutorial, I immediately thought of this proposal when I read the "Adding dependencies" section and didn't see mention of the convenient commands included in this proposal. I know I was excited when this proposal was accepted, as Im sure many others were and would love to be able to make the process of package manifest editing easier and more approachable. I know there hasn't been much in this PR lately, but if available, any update would be great! |
can be closed |
Did this feature never make it? |
IIUC there are no people currently working on the implementation. |
This is basically bringing back @owenv's work from https://github.com/owenv/swift-package-editor into SwiftPM itself. Previously, we didn't have a way to use swift-syntax from the toolchain, but with the new
SwiftParser
library, it has now become independent of the toolchain and that problem has been solved.I talked to @owenv, updated his code/tests for the newer APIs in swift-syntax and SwiftPM. Right now, I have not brought over the integration tests, yet, only the unit tests. I will also have to do some updates to support newer manifests versions, e.g. by adding a way to use registry dependencies.
In contrast to the original proposal, I'm keeping this in a separate top-level command called
swift package-editor
. This is up for discussion, but I think it would be good to start moduralizing the CLI a bit more to avoid having to build every dependency and module in the first CMake stage. That would require taking aprt theCommands
module eventually, for now I am just opting to not add more functionality into that since it just makes everything very monolithic.