Skip to content

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

Closed
wants to merge 1 commit into from
Closed

Bring back the implementation of SE-0301 #5802

wants to merge 1 commit into from

Conversation

neonichu
Copy link
Contributor

@neonichu neonichu commented Oct 8, 2022

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 the Commands module eventually, for now I am just opting to not add more functionality into that since it just makes everything very monolithic.

@neonichu neonichu self-assigned this Oct 8, 2022
@neonichu
Copy link
Contributor Author

neonichu commented Oct 8, 2022

@swift-ci please smoke test

@neonichu
Copy link
Contributor Author

neonichu commented Oct 8, 2022

Looks like there's an issue with compiling with 5.6 again here.

@neonichu neonichu requested a review from owenv October 8, 2022 01:06
Copy link
Contributor

@owenv owenv left a 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 @@
/*
Copy link
Contributor

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)
Copy link
Contributor

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([
Copy link
Contributor

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 @@
/*
Copy link
Contributor

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 @@
/*
Copy link
Contributor

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(
Copy link
Contributor

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]>
@neonichu
Copy link
Contributor Author

Ah nice, I didn't know about Co-authored-by, added now!

@@ -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)),
Copy link
Contributor

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 {
Copy link
Contributor

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() }
Copy link
Contributor

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)!
Copy link
Contributor

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")
Copy link
Contributor

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
Copy link
Contributor

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)!
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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")
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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")
Copy link
Contributor

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 {
Copy link
Contributor

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 {
Copy link
Contributor

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
Copy link
Contributor

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 {
Copy link
Contributor

@tomerd tomerd Oct 12, 2022

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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DiagnosticsEngine 😱

Copy link
Contributor

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 {
Copy link
Contributor

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)
Copy link
Contributor

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))
Copy link
Contributor

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)
Copy link
Contributor

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(
Copy link
Contributor

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:)])
Copy link
Contributor

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)
Copy link
Contributor

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 }
Copy link
Contributor

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
Copy link
Contributor

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 })
Copy link
Contributor

@tomerd tomerd Oct 12, 2022

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)
}
}
Copy link
Contributor

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) {
Copy link
Contributor

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>(
Copy link
Contributor

@tomerd tomerd Oct 12, 2022

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...) {
Copy link
Contributor

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 {
Copy link
Contributor

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

@tomerd
Copy link
Contributor

tomerd commented Oct 12, 2022

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

@tomerd tomerd added the WIP Work in progress label Oct 17, 2022
@neonichu
Copy link
Contributor Author

neonichu commented Dec 8, 2022

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 swift package-editor, contrary to the current proposal? If not, I guess we could have swift-package shell out to the other command for now? Eventually, once we can bootstrap on Windows, we could move the implementation over to swift-package, but right now we have to avoid it because we need to retain the ability of building swift-package using CMake.

cc @tomerd

@aaronjedwards
Copy link

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!

@neonichu
Copy link
Contributor Author

can be closed

@neonichu neonichu closed this Nov 11, 2023
@Diggory
Copy link
Contributor

Diggory commented Nov 30, 2023

Did this feature never make it?

@MaxDesiatov
Copy link
Contributor

IIUC there are no people currently working on the implementation.

@MaxDesiatov MaxDesiatov reopened this Jan 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
WIP Work in progress
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants