-
Notifications
You must be signed in to change notification settings - Fork 92
Introduce subcommand for jextract to prevent linking Foundation #624
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
base: main
Are you sure you want to change the base?
Changes from all commits
e88fe4d
1aae232
d792ca9
08baa6d
7f1353f
fbb9bd8
fa97456
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,7 +139,11 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { | |
| arguments += [ | ||
| "--generated-java-sources-list-file-output", javaSourcesListFileName, | ||
| ] | ||
| jextractOutputFiles += [javaSourcesFile] | ||
| // NOTE: javaSourcesFile is intentionally NOT added to jextractOutputFiles. | ||
| // Adding a non-Swift file as a build command output causes SPM to bundle it | ||
| // as a module resource, which triggers resource_bundle_accessor.swift generation | ||
| // and pulls in Foundation.Bundle. The file is still written by the tool as a | ||
| // side-effect; the java-callbacks-build command reads it by its known path. | ||
| } | ||
|
|
||
| commands += [ | ||
|
|
@@ -148,7 +152,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { | |
| executable: toolURL, | ||
| arguments: arguments, | ||
| inputFiles: [configFile] + swiftFiles, | ||
| outputFiles: jextractOutputFiles | ||
| outputFiles: jextractOutputFiles, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pretty sure formatter will dislike this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it passed 🤷
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. okey I'm honestly confused with swift-format sometimes... 😅 |
||
| ) | ||
| ] | ||
|
|
||
|
|
@@ -188,85 +192,48 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { | |
| ?? swiftJavaDirectory.appending(path: "gradlew") // fallback to calling ./gradlew if gradle is not installed | ||
| log("Detected 'gradle' executable (or gradlew fallback): \(gradleExecutable)") | ||
|
|
||
| commands += [ | ||
| .buildCommand( | ||
| displayName: "Build SwiftKitCore using Gradle (Java)", | ||
| executable: gradleExecutable, | ||
| arguments: [ | ||
| ":SwiftKitCore:build", | ||
| "--project-dir", swiftJavaDirectory.path(percentEncoded: false), | ||
| "--gradle-user-home", gradleUserHomePath, | ||
| "--configure-on-demand", | ||
| "--no-daemon", | ||
| ], | ||
| environment: gradlewEnvironment, | ||
| inputFiles: [swiftJavaDirectory], | ||
| outputFiles: [swiftKitCoreClassPath] | ||
| ) | ||
| ] | ||
|
|
||
| // Compile the jextracted sources | ||
| let javaHome = URL(filePath: findJavaHome()) | ||
|
|
||
| commands += [ | ||
| .buildCommand( | ||
| displayName: "Build extracted Java sources", | ||
| executable: | ||
| javaHome | ||
| .appending(path: "bin") | ||
| .appending(path: self.javacName), | ||
| arguments: [ | ||
| "@\(javaSourcesFile.path(percentEncoded: false))", | ||
| "-d", javaCompiledClassesURL.path(percentEncoded: false), | ||
| "-parameters", | ||
| "-classpath", swiftKitCoreClassPath.path(percentEncoded: false), | ||
| ], | ||
| inputFiles: [javaSourcesFile, swiftKitCoreClassPath], | ||
| outputFiles: [javaCompiledClassesURL] | ||
| ) | ||
| ] | ||
|
|
||
| // Run `configure` to extract a swift-java config to use for wrap-java | ||
| let swiftJavaConfigURL = context.pluginWorkDirectoryURL.appending(path: "swift-java.config") | ||
|
|
||
| commands += [ | ||
| .buildCommand( | ||
| displayName: "Output swift-java.config that contains all extracted Java sources", | ||
| executable: toolURL, | ||
| arguments: [ | ||
| "configure", | ||
| "--output-directory", context.pluginWorkDirectoryURL.path(percentEncoded: false), | ||
| "--cp", javaCompiledClassesURL.path(percentEncoded: false), | ||
| "--swift-module", sourceModule.name, | ||
| "--swift-type-prefix", "Java", | ||
| ], | ||
| inputFiles: [javaCompiledClassesURL], | ||
| outputFiles: [swiftJavaConfigURL] | ||
| ) | ||
| ] | ||
| let javacPath = | ||
| javaHome | ||
| .appending(path: "bin") | ||
| .appending(path: self.javacName) | ||
|
|
||
| let singleSwiftFileOutputName = "WrapJavaGenerated.swift" | ||
|
|
||
| // In the end we can run wrap-java on the previous inputs | ||
| var wrapJavaArguments = [ | ||
| "wrap-java", | ||
| let javaCallbacksSwiftOutput = outputSwiftDirectory.appending(path: singleSwiftFileOutputName) | ||
|
|
||
| // Combine gradle + javac + configure + wrap-java into a single command that | ||
| // declares only a Swift file as its output. This avoids SPM treating any | ||
| // intermediate artifact (Gradle output directories, compiled .class files, | ||
| // swift-java.config) as module resources, which would trigger | ||
| // resource_bundle_accessor.swift generation and pull Foundation.Bundle into | ||
| // the target binary. | ||
| // | ||
| // inputFiles includes the Swift outputs from jextract so that SPM knows | ||
| // this command must run after jextract finishes. | ||
| var javaCallbacksArguments = [ | ||
| "java-callbacks-build", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we HAVE to do it all int one huge mega step? Can we not invoke multiple commands one by one? I'd like to make this hidden in command line somehow...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, what would we benefit from writing multiple commands? As this is only intended for internal use and a workaround for the SwiftPM limitation. |
||
| "--swift-java-tool", toolURL.path(percentEncoded: false), | ||
| "--gradle-executable", gradleExecutable.path(percentEncoded: false), | ||
| "--gradle-project-dir", swiftJavaDirectory.path(percentEncoded: false), | ||
| "--gradle-user-home", gradleUserHomePath, | ||
| "--javac", javacPath.path(percentEncoded: false), | ||
| "--java-sources-list", javaSourcesFile.path(percentEncoded: false), | ||
| "--java-output-directory", javaCompiledClassesURL.path(percentEncoded: false), | ||
| "--swift-kit-core-classpath", swiftKitCoreClassPath.path(percentEncoded: false), | ||
| "--swift-module", sourceModule.name, | ||
| "--swift-type-prefix", "Java", | ||
| "--output-directory", outputSwiftDirectory.path(percentEncoded: false), | ||
| "--config", swiftJavaConfigURL.path(percentEncoded: false), | ||
| "--cp", swiftKitCoreClassPath.path(percentEncoded: false), | ||
| "--single-swift-file-output", singleSwiftFileOutputName, | ||
| ] | ||
|
|
||
| // Add any dependent config files as arguments | ||
| wrapJavaArguments += dependentConfigFilesArguments | ||
| javaCallbacksArguments += dependentConfigFilesArguments | ||
|
|
||
| commands += [ | ||
| .buildCommand( | ||
| displayName: "Wrap compiled Java sources using wrap-java", | ||
| displayName: "Build SwiftKitCore, compile Java callbacks, and generate Swift wrappers", | ||
| executable: toolURL, | ||
| arguments: wrapJavaArguments, | ||
| inputFiles: [swiftJavaConfigURL, swiftKitCoreClassPath], | ||
| outputFiles: [outputSwiftDirectory.appending(path: singleSwiftFileOutputName)] | ||
| arguments: javaCallbacksArguments, | ||
| inputFiles: outputSwiftFiles + [swiftJavaDirectory], | ||
| outputFiles: [javaCallbacksSwiftOutput], | ||
| ) | ||
| ] | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This source file is part of the Swift.org open source project | ||
| // | ||
| // Copyright (c) 2024 Apple Inc. and the Swift.org project authors | ||
| // Licensed under Apache License v2.0 | ||
| // | ||
| // See LICENSE.txt for license information | ||
| // See CONTRIBUTORS.txt for the list of Swift.org project authors | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| import ArgumentParser | ||
| import Foundation | ||
| import Subprocess | ||
|
|
||
| #if canImport(System) | ||
| import System | ||
| #else | ||
| @preconcurrency import SystemPackage | ||
| #endif | ||
|
|
||
| extension SwiftJava { | ||
| /// Builds Swift-Java callbacks in a single command: | ||
| /// 1. Building SwiftKitCore with Gradle | ||
| /// 2. Compiling extracted Java sources with javac | ||
| /// 3. Running `swift-java configure` to produce a swift-java.config | ||
| /// 4. Running `swift-java wrap-java` to generate Swift wrappers | ||
| /// | ||
| /// **WORKAROUND**: rdar://172649681 if we invoke commands one by one with java outputs SwiftPM will link Foundation | ||
| /// | ||
| /// This command is used by ``JExtractSwiftPlugin`` to consolidate all of the above | ||
| /// into a single build command that declares only a Swift file as its output, | ||
| /// avoiding SPM treating intermediate Java artifacts (compiled classes, config files, | ||
| /// Gradle output directories) as module resources, which would trigger | ||
| /// resource_bundle_accessor.swift generation and pull Foundation.Bundle into the binary. | ||
| struct JavaCallbacksBuildCommand: AsyncParsableCommand { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Man I really dislike this command... we'll have to copy around and keep it updated constantly :-\ Before we merge this, can we make sure we can't avoid it? How about just ignoring the java outputs, does swiftpm prevent things from working if we just pretend they don't exist but we write them anyway?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem is I am guessing that SwiftPM uses the input/output to derive the dependencies between the build commands. So, I am afraid that it will run the builds in the wrong order. Alternatively, if its only using it for stale checks, that means that if we remove them from the outputs, we would retrigger those commands on each invocation of the build plugin. Maybe someone from the SwiftPM team can confirm the behaviour here? |
||
|
|
||
| static let configuration = CommandConfiguration( | ||
| commandName: "java-callbacks-build", | ||
| abstract: | ||
| "Build SwiftKitCore, compile Java callbacks, and generate Swift wrappers (for use by JExtractSwiftPlugin)", | ||
| shouldDisplay: false, | ||
| ) | ||
|
|
||
| // MARK: Gradle options | ||
|
|
||
| @Option(help: "Path to the gradle (or gradlew) executable") | ||
| var gradleExecutable: String | ||
|
|
||
| @Option(help: "The Gradle project directory (passed as --project-dir)") | ||
| var gradleProjectDir: String | ||
|
|
||
| @Option(help: "The Gradle user home directory (GRADLE_USER_HOME)") | ||
| var gradleUserHome: String | ||
|
|
||
| // MARK: javac options | ||
|
|
||
| @Option(help: "Path to the javac executable") | ||
| var javac: String | ||
|
|
||
| @Option(help: "Path to the @-file listing Java sources to compile") | ||
| var javaSourcesList: String | ||
|
|
||
| @Option(help: "Directory where compiled Java classes should be output") | ||
| var javaOutputDirectory: String | ||
|
|
||
| @Option(help: "Path to SwiftKitCore compiled classes (classpath for javac and wrap-java)") | ||
| var swiftKitCoreClasspath: String | ||
|
|
||
| // MARK: Swift generation options | ||
|
|
||
| @Option(help: "The name of the Swift module") | ||
| var swiftModule: String | ||
|
|
||
| @Option(help: "Prefix to add to generated Swift type names") | ||
| var swiftTypePrefix: String? | ||
|
|
||
| @Option( | ||
| name: .customLong("output-directory"), | ||
| help: "Directory where generated Swift files should be written", | ||
| ) | ||
| var outputDirectory: String | ||
|
|
||
| @Option(help: "Name of the single Swift output file") | ||
| var singleSwiftFileOutput: String | ||
|
|
||
| @Option(help: "Path to the swift-java tool executable (used to invoke subcommands)") | ||
| var swiftJavaTool: String | ||
|
|
||
| @Option( | ||
| help: | ||
| "Dependent module configurations (format: ModuleName=/path/to/swift-java.config)" | ||
| ) | ||
| var dependsOn: [String] = [] | ||
|
|
||
| mutating func run() async throws { | ||
| let outputDir = URL(fileURLWithPath: outputDirectory) | ||
| let outputFile = outputDir.appendingPathComponent(singleSwiftFileOutput) | ||
|
|
||
| // 1. Build SwiftKitCore using Gradle. | ||
| try await runSubprocess( | ||
| executable: gradleExecutable, | ||
| arguments: [ | ||
| ":SwiftKitCore:build", | ||
| "--project-dir", gradleProjectDir, | ||
| "--gradle-user-home", gradleUserHome, | ||
| "--configure-on-demand", | ||
| "--no-daemon", | ||
| ], | ||
| environment: .inherit.updating(["GRADLE_USER_HOME": gradleUserHome]), | ||
| errorMessage: "gradle :SwiftKitCore:build", | ||
| ) | ||
|
|
||
| // If the sources list does not exist, jextract produced no Java callbacks. | ||
| // Write an empty placeholder Swift file and return early. | ||
| guard FileManager.default.fileExists(atPath: javaSourcesList) else { | ||
| try FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) | ||
| try "// No Java callbacks generated\n".write( | ||
| to: outputFile, | ||
| atomically: true, | ||
| encoding: .utf8, | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| // 2. Compile Java sources with javac. | ||
| try FileManager.default.createDirectory( | ||
| atPath: javaOutputDirectory, | ||
| withIntermediateDirectories: true, | ||
| ) | ||
|
|
||
| try await runSubprocess( | ||
| executable: javac, | ||
| arguments: [ | ||
| "@\(javaSourcesList)", | ||
| "-d", javaOutputDirectory, | ||
| "-parameters", | ||
| "-classpath", swiftKitCoreClasspath, | ||
| ], | ||
| errorMessage: "javac", | ||
| ) | ||
|
|
||
| // 3. Generate swift-java.config from compiled classes. | ||
| // Written into javaOutputDirectory (inside pluginWorkDirectory) but NOT | ||
| // declared as a build command output, so SPM will not bundle it as a resource. | ||
| var configureArgs = [ | ||
| "configure", | ||
| "--output-directory", javaOutputDirectory, | ||
| "--cp", javaOutputDirectory, | ||
| "--swift-module", swiftModule, | ||
| ] | ||
| if let prefix = swiftTypePrefix { | ||
| configureArgs += ["--swift-type-prefix", prefix] | ||
| } | ||
|
|
||
| try await runSubprocess( | ||
| executable: swiftJavaTool, | ||
| arguments: configureArgs, | ||
| errorMessage: "swift-java configure", | ||
| ) | ||
|
|
||
| // 4. Generate Swift wrappers using wrap-java. | ||
| let configPath = URL(fileURLWithPath: javaOutputDirectory) | ||
| .appendingPathComponent("swift-java.config").path | ||
|
|
||
| var wrapJavaArgs = [ | ||
| "wrap-java", | ||
| "--swift-module", swiftModule, | ||
| "--output-directory", outputDirectory, | ||
| "--config", configPath, | ||
| "--cp", swiftKitCoreClasspath, | ||
| "--single-swift-file-output", singleSwiftFileOutput, | ||
| ] | ||
| wrapJavaArgs += dependsOn.flatMap { ["--depends-on", $0] } | ||
|
|
||
| try await runSubprocess( | ||
| executable: swiftJavaTool, | ||
| arguments: wrapJavaArgs, | ||
| errorMessage: "swift-java wrap-java", | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Helpers | ||
|
|
||
| private func runSubprocess( | ||
| executable: String, | ||
| arguments: [String], | ||
| environment: Subprocess.Environment = .inherit, | ||
| errorMessage: String, | ||
| ) async throws { | ||
| let result = try await Subprocess.run( | ||
| .path(FilePath(executable)), | ||
| arguments: .init(arguments), | ||
| environment: environment, | ||
| output: .standardOutput, | ||
| error: .standardError, | ||
| ) | ||
| guard result.terminationStatus.isSuccess else { | ||
| throw JavaCallbacksBuildError( | ||
| "\(errorMessage) failed with exit status \(result.terminationStatus)" | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| struct JavaCallbacksBuildError: Error, CustomStringConvertible { | ||
| let description: String | ||
| init(_ message: String) { self.description = message } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,7 @@ struct SwiftJava: AsyncParsableCommand { | |
| ResolveCommand.self, | ||
| WrapJavaCommand.self, | ||
| JExtractCommand.self, | ||
| JavaCallbacksBuildCommand.self, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you check how we can hide this form
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what the |
||
| ] | ||
| ) | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
Ugh oh no... ;-(