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