1+ //===----------------------------------------------------------------------===//
2+ //
3+ // This source file is part of the Swift.org open source project
4+ //
5+ // Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+ // Licensed under Apache License v2.0
7+ //
8+ // See LICENSE.txt for license information
9+ // See CONTRIBUTORS.txt for the list of Swift.org project authors
10+ //
11+ // SPDX-License-Identifier: Apache-2.0
12+ //
13+ //===----------------------------------------------------------------------===//
14+
15+ import ArgumentParser
16+ import Foundation
17+ import SwiftJavaLib
18+ import JExtractSwiftLib
19+ import JavaKit
20+ import JavaKitJar
21+ import JavaKitNetwork
22+ import JavaKitReflection
23+ import SwiftSyntax
24+ import SwiftSyntaxBuilder
25+ import JavaKitConfigurationShared
26+ import JavaKitShared
27+
28+ extension SwiftJava {
29+ struct ConfigureCommand : SwiftJavaBaseAsyncParsableCommand , HasCommonOptions , HasCommonJVMOptions {
30+ static let configuration = CommandConfiguration (
31+ commandName: " configure " ,
32+ abstract: " Configure and emit a swift-java.config file based on an input dependency or jar file " )
33+
34+ @OptionGroup var commonOptions : SwiftJava . CommonOptions
35+ @OptionGroup var commonJVMOptions : SwiftJava . CommonJVMOptions
36+
37+ // TODO: This should be a "make wrappers" option that just detects when we give it a jar
38+ @Flag (
39+ help: " Specifies that the input is a *.jar file whose public classes will be loaded. The output of swift-java will be a configuration file (swift-java.config) that can be used as input to a subsequent swift-java invocation to generate wrappers for those public classes. "
40+ )
41+ var jar : Bool = false
42+
43+ @Option (
44+ name: . long,
45+ help: " How to handle an existing swift-java.config; by default 'overwrite' by can be changed to amending a configuration "
46+ )
47+ var existingConfigFile : ExistingConfigFileMode = . overwrite
48+ enum ExistingConfigFileMode : String , ExpressibleByArgument , Codable {
49+ case overwrite
50+ case amend
51+ }
52+
53+ @Option ( help: " The name of the Swift module into which the resulting Swift types will be generated. " )
54+ var swiftModule : String
55+
56+ var effectiveSwiftModule : String {
57+ swiftModule
58+ }
59+
60+ @Argument (
61+ help: " The input file, which is either a swift-java configuration file or (if '-jar' was specified) a Jar file. "
62+ )
63+ var input : String ?
64+ }
65+ }
66+
67+ extension SwiftJava . ConfigureCommand {
68+ mutating func runSwiftJavaCommand( config: inout Configuration ) async throws {
69+ // Form a class path from all of our input sources:
70+ // * Command-line option --classpath
71+ let classpathOptionEntries : [ String ] = self . commonJVMOptions. classpath. flatMap { $0. split ( separator: " : " ) . map ( String . init) }
72+ let classpathFromEnv = ProcessInfo . processInfo. environment [ " CLASSPATH " ] ? . split ( separator: " : " ) . map ( String . init) ?? [ ]
73+ let classpathFromConfig : [ String ] = config. classpath? . split ( separator: " : " ) . map ( String . init) ?? [ ]
74+ print ( " [debug][swift-java] Base classpath from config: \( classpathFromConfig) " )
75+
76+ var classpathEntries : [ String ] = classpathFromConfig
77+
78+ let swiftJavaCachedModuleClasspath = findSwiftJavaClasspaths ( in:
79+ // self.effectiveCacheDirectory ??
80+ FileManager . default. currentDirectoryPath)
81+ print ( " [debug][swift-java] Classpath from *.swift-java.classpath files: \( swiftJavaCachedModuleClasspath) " )
82+ classpathEntries += swiftJavaCachedModuleClasspath
83+
84+ if !classpathOptionEntries. isEmpty {
85+ print ( " [debug][swift-java] Classpath from options: \( classpathOptionEntries) " )
86+ classpathEntries += classpathOptionEntries
87+ } else {
88+ // * Base classpath from CLASSPATH env variable
89+ print ( " [debug][swift-java] Classpath from environment: \( classpathFromEnv) " )
90+ classpathEntries += classpathFromEnv
91+ }
92+
93+ let extraClasspath = input ?? " " // FIXME: just use the -cp as usual
94+ let extraClasspathEntries = extraClasspath. split ( separator: " : " ) . map ( String . init)
95+ print ( " [debug][swift-java] Extra classpath: \( extraClasspathEntries) " )
96+ classpathEntries += extraClasspathEntries
97+
98+ // Bring up the Java VM when necessary
99+
100+ if logLevel >= . debug {
101+ let classpathString = classpathEntries. joined ( separator: " : " )
102+ print ( " [debug][swift-java] Initialize JVM with classpath: \( classpathString) " )
103+ }
104+ let jvm = try JavaVirtualMachine . shared ( classpath: classpathEntries)
105+
106+ try emitConfiguration ( classpath: self . commonJVMOptions. classpath, environment: jvm. environment ( ) )
107+ }
108+
109+ /// Get base configuration, depending on if we are to 'amend' or 'overwrite' the existing configuration.
110+ func getBaseConfigurationForWrite( ) throws -> ( Bool , Configuration ) {
111+ guard let actualOutputDirectory = self . actualOutputDirectory else {
112+ // If output has no path there's nothing to amend
113+ return ( false , . init( ) )
114+ }
115+
116+ switch self . existingConfigFile {
117+ case . overwrite:
118+ // always make up a fresh instance if we're overwriting
119+ return ( false , . init( ) )
120+ case . amend:
121+ let configPath = actualOutputDirectory
122+ guard let config = try readConfiguration ( sourceDir: configPath. path) else {
123+ return ( false , . init( ) )
124+ }
125+ return ( true , config)
126+ }
127+ }
128+
129+ // TODO: make this perhaps "emit type mappings"
130+ mutating func emitConfiguration(
131+ classpath: [ String ] ,
132+ environment: JNIEnvironment
133+ ) throws {
134+ if let filterJavaPackage = self . commonJVMOptions. filterJavaPackage {
135+ print ( " [java-swift][debug] Generate Java->Swift type mappings. Active filter: \( filterJavaPackage) " )
136+ }
137+ print ( " [java-swift][debug] Classpath: \( classpath) " )
138+
139+ if classpath. isEmpty {
140+ print ( " [java-swift][warning] Classpath is empty! " )
141+ }
142+
143+ // Get a fresh or existing configuration we'll amend
144+ var ( amendExistingConfig, configuration) = try getBaseConfigurationForWrite ( )
145+ if amendExistingConfig {
146+ print ( " [swift-java] Amend existing swift-java.config file... " )
147+ }
148+ configuration. classpath = classpath. joined ( separator: " : " ) // TODO: is this correct?
149+
150+ // Import types from all the classpath entries;
151+ // Note that we use the package level filtering, so users have some control over what gets imported.
152+ let classpathEntries = classpath. split ( separator: " : " ) . map ( String . init)
153+ for entry in classpathEntries {
154+ guard fileOrDirectoryExists ( at: entry) else {
155+ // We only log specific jars missing, as paths may be empty directories that won't hurt not existing.
156+ print ( " [debug][swift-java] Classpath entry does not exist: \( entry) " )
157+ continue
158+ }
159+
160+ print ( " [debug][swift-java] Importing classpath entry: \( entry) " )
161+ if entry. hasSuffix ( " .jar " ) {
162+ let jarFile = try JarFile ( entry, false , environment: environment)
163+ try addJavaToSwiftMappings (
164+ to: & configuration,
165+ forJar: jarFile,
166+ environment: environment
167+ )
168+ } else if FileManager . default. fileExists ( atPath: entry) {
169+ print ( " [warning][swift-java] Currently unable handle directory classpath entries for config generation! Skipping: \( entry) " )
170+ } else {
171+ print ( " [warning][swift-java] Classpath entry does not exist, skipping: \( entry) " )
172+ }
173+ }
174+
175+ // Encode the configuration.
176+ let contents = try configuration. renderJSON ( )
177+
178+ // Write the file.
179+ try writeContents (
180+ contents,
181+ to: " swift-java.config " ,
182+ description: " swift-java configuration file "
183+ )
184+ }
185+
186+ mutating func addJavaToSwiftMappings(
187+ to configuration: inout Configuration ,
188+ forJar jarFile: JarFile ,
189+ environment: JNIEnvironment
190+ ) throws {
191+ for entry in jarFile. entries ( ) ! {
192+ // We only look at class files in the Jar file.
193+ guard entry. getName ( ) . hasSuffix ( " .class " ) else {
194+ continue
195+ }
196+
197+ // Skip some "common" files we know that would be duplicated in every jar
198+ guard !entry. getName ( ) . hasPrefix ( " META-INF " ) else {
199+ continue
200+ }
201+ guard !entry. getName ( ) . hasSuffix ( " package-info " ) else {
202+ continue
203+ }
204+ guard !entry. getName ( ) . hasSuffix ( " package-info.class " ) else {
205+ continue
206+ }
207+
208+ // If this is a local class, it cannot be mapped into Swift.
209+ if entry. getName ( ) . isLocalJavaClass {
210+ continue
211+ }
212+
213+ let javaCanonicalName = String ( entry. getName ( ) . replacing ( " / " , with: " . " )
214+ . dropLast ( " .class " . count) )
215+
216+ if let filterJavaPackage = self . commonJVMOptions. filterJavaPackage,
217+ !javaCanonicalName. hasPrefix ( filterJavaPackage) {
218+ // Skip classes which don't match our expected prefix
219+ continue
220+ }
221+
222+ if configuration. classes ? [ javaCanonicalName] != nil {
223+ // We never overwrite an existing class mapping configuration.
224+ // E.g. the user may have configured a custom name for a type.
225+ continue
226+ }
227+
228+ configuration. classes ? [ javaCanonicalName] =
229+ javaCanonicalName. defaultSwiftNameForJavaClass
230+ }
231+ }
232+
233+ }
234+
235+ package func fileOrDirectoryExists( at path: String ) -> Bool {
236+ var isDirectory : ObjCBool = false
237+ return FileManager . default. fileExists ( atPath: path, isDirectory: & isDirectory)
238+ }
0 commit comments