├── .gitignore ├── .swift-version ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── LICENSE ├── Package.swift ├── README.md └── Sources ├── ExampleConfig └── ExampleConfig.swift ├── ExampleMain └── main.swift ├── PackageConfig ├── DynamicLibraries.swift ├── Error.swift ├── Loader.swift ├── Package.swift ├── PackageConfig.swift ├── PackageConfiguration.swift └── Writer.swift ├── PackageConfigExecutable └── main.swift └── PackageConfigs └── PackageConfigs.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | /*.xcodeproj 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.2 2 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: 2 | directories: 3 | # General SwiftPM 4 | - .build 5 | # Danger Swift plugins 6 | - ~/.danger-swift 7 | 8 | language: generic 9 | 10 | matrix: 11 | include: 12 | - os: osx 13 | osx_image: xcode10 14 | script: 15 | - swift build 16 | - swift run package-config-example --verbose 17 | 18 | - os: osx 19 | osx_image: xcode11 20 | script: 21 | - swift build 22 | - swift run package-config-example --verbose 23 | 24 | - os: linux 25 | language: generic 26 | sudo: required 27 | dist: trusty 28 | install: 29 | - eval "$(curl -sL https://swiftenv.fuller.li/install.sh)" 30 | - swiftenv global 4.2 31 | script: 32 | - swift build 33 | - swift run package-config-example --verbose 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 onwards: @orta + @fmeloni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "PackageConfig", 8 | products: [ 9 | .library(name: "PackageConfig", type: .dynamic, targets: ["PackageConfig"]), 10 | .executable(name: "package-config", targets: ["PackageConfigExecutable"]), // dev 11 | 12 | .library(name: "ExampleConfig", type: .dynamic, targets: ["ExampleConfig"]), // dev 13 | .executable(name: "package-config-example", targets: ["ExampleMain"]), // dev 14 | ], 15 | dependencies: [ 16 | ], 17 | targets: [ 18 | .target(name: "PackageConfig", dependencies: []), 19 | .target(name: "PackageConfigExecutable", dependencies: []), // dev 20 | 21 | .target(name: "ExampleConfig", dependencies: ["PackageConfig"]), // dev 22 | .target(name: "ExampleMain", dependencies: ["PackageConfig", "ExampleConfig"]), // dev 23 | 24 | .target(name: "PackageConfigs", dependencies: ["ExampleConfig"]), // dev 25 | ] 26 | ) 27 | 28 | #if canImport(ExampleConfig) 29 | import ExampleConfig 30 | 31 | let example = ExampleConfig(value: "example value").write() 32 | #endif 33 | 34 | #if canImport(PackageConfig) 35 | import PackageConfig 36 | 37 | let config = PackageConfiguration(["example": [ 38 | ["example1": ""], 39 | "example2", 40 | 3 41 | ]]).write() 42 | #endif 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PackageConfig 2 | 3 | A Swift Package that allows you to define configuration settings inside a `Package.swift` - this is so that tools can all keep their configs consolidated inside a single place. 4 | 5 | Tool builders use this dependency to grab their config settings. 6 | 7 | ## Package Configuration 8 | 9 | The fastest way to insert a configuration in your `Package.swift` is to add `PackageConfig` to your dependencies 10 | 11 | ```swift 12 | .package(url: "https://github.com/shibapm/PackageConfig.git", from: "0.13.0") 13 | ``` 14 | 15 | And add the configuration right at the bottom of your `Package.swift` 16 | 17 | e.g. 18 | 19 | ```swift 20 | #if canImport(PackageConfig) 21 | import PackageConfig 22 | 23 | let config = PackageConfiguration([ 24 | "komondor": [ 25 | "pre-push": "swift test", 26 | "pre-commit": [ 27 | "swift test", 28 | "swift run swiftformat .", 29 | "swift run swiftlint autocorrect --path Sources/", 30 | "git add .", 31 | ], 32 | ], 33 | "rocket": [ 34 | "after": [ 35 | "push", 36 | ], 37 | ], 38 | ]).write() 39 | #endif 40 | ``` 41 | 42 | ## Custom Configuration Types 43 | 44 | `PackageConfig` offers also the possibility to create your own configuration type 45 | 46 | ### User writes: 47 | 48 | Run this line to have empty source for `PackageConfigs` target generated for you. 49 | 50 | ```bash 51 | swift run package-config 52 | ``` 53 | 54 | First time it should return an error `error: no target named 'PackageConfigs'`. 55 | 56 | Now you can list all the required package configs anywhere in the list of targets in `Package.swift` like this. 57 | 58 | ```swift 59 | // PackageConfig parses PackageConfigs target in Package.swift to extract list of dylibs to link when compiling Package.swift with configurations 60 | .target(name: "PackageConfigs", dependencies: [ 61 | "ExampleConfig" // some executable configuration definition dylib 62 | ]) 63 | ``` 64 | 65 | At the very bottom of the `Package.swift` 66 | 67 | ```swift 68 | #if canImport(ExampleConfig) // example config dynamic library 69 | import ExampleConfig 70 | 71 | // invoking write is mandatory, otherwise the config won't be written // thanks captain obvious 72 | let exampleConfig = ExampleConfig(value: "example value").write() 73 | #endif 74 | ``` 75 | 76 | If more than one dependency uses `PackageConfig` be sure to wrap each in 77 | 78 | ```swift 79 | #if canImport(SomeLibraryConfig) 80 | import SomeLibraryConfig 81 | 82 | let someLibraryConfig = SomeLibraryConfig().write() 83 | #endif 84 | ``` 85 | 86 | Be sure to invoke `write` method of the `Config` otherwise this won't work. 87 | 88 | And then to use executable user would need to run this in the same directory as his/her project `Package.swift` 89 | 90 | ```bash 91 | swift run package-config # compiles PackageConfigs target, expecting to find a dylib in `.build` directory for each of the listed libraries configs 92 | swift run example # runs your library executable 93 | ``` 94 | 95 | ### Tool-dev writes: 96 | 97 | For the sake of example lets assume your library is called **Example** then `Package.swift` would look like this: 98 | 99 | ```swift 100 | let package = Package( 101 | name: "Example", 102 | products: [ 103 | // notice that product with your library config should be dynamic library in order to produce dylib and allow PackageConfig to link it when building Package.swift 104 | .library(name: "ExampleConfig", type: .dynamic, targets: ["ExampleConfig"]), 105 | // 106 | .executable(name: "example", targets: ["Example"]), 107 | ], 108 | dependencies: [ 109 | .package(url: "https://github.com/shibapm/PackageConfig.git", from: "0.0.2"), 110 | ], 111 | targets: [ 112 | .target(name: "ExampleConfig", dependencies: ["PackageConfig"]), 113 | .target(name: "Example", dependencies: ["ExampleConfig"]), 114 | ] 115 | ) 116 | ``` 117 | 118 | In your `ExampleConfig` target define `ExampleConfig` like this. 119 | 120 | ```swift 121 | import PackageConfig 122 | 123 | // it must be public for you to use in your executable target 124 | // also you must conform to `Codable` and `PackageConfig` 125 | public struct ExampleConfig: Codable, PackageConfig { 126 | 127 | // here can be whatever you want as long as your config can stay `Codable` 128 | let value: String 129 | 130 | // here you must define your config fileName which will be used to write and read it to/from temporary directory 131 | public static var fileName: String { return "example-config.json" } 132 | 133 | // public init is also a requirement 134 | public init(value: String) { 135 | self.value = value 136 | } 137 | } 138 | ``` 139 | 140 | Then for example in your `main.swift` in executable `Example` target you can load your config like this: 141 | 142 | ```swift 143 | import ExampleConfig 144 | 145 | do { 146 | let config = try ExampleConfig.load() 147 | print(config) 148 | } catch { 149 | print(error) 150 | } 151 | ``` 152 | 153 | ### Notes for library developers 154 | 155 | Since `YourConfig` target is a dynamic library you must ensure that you have built it everytime when using either `read` or `write` methods of `PackageConfig`. When building from terminal this can be done by just running `swift build`. 156 | 157 | ---- 158 | 159 | # Changelog 160 | 161 | - 0.0.1 162 | 163 | Exposes a config for `Package.swift` files 164 | 165 | ```swift 166 | #if canImport(PackageConfig) 167 | import PackageConfig 168 | 169 | let config = PackageConfig([ 170 | "danger" : ["disable"], 171 | "linter": ["rules": ["allowSomething"]] 172 | ]).write() 173 | #endif 174 | ``` 175 | 176 | This might be everything, so if in a month or two nothing really changes 177 | I'll v1 after this release. 178 | 179 | # How it all works 180 | 181 | When you invoke `YourPackage.load()` it will compile the `Package.swift` in the current directory using `swiftc`. 182 | 183 | While compiling it will try to link list of dynamic libraries listed in `PackageConfigs` target. 184 | 185 | When it is compiled, PackageConfig will run and when `YourPackage.write()` is called your package configuration json will be written to temporary directory. 186 | 187 | After that it will try to read the json and decode it as if it was `YourPackage` type, providing it back to where you have invoked `load` method. 188 | 189 | # Debugging 190 | 191 | ### How to see the JSON from a Package.swift file 192 | 193 | Use SPM with verbose mode: 194 | 195 | ```sh 196 | ~/d/p/o/i/s/PackageConfig $ swift build --verbose 197 | ``` 198 | 199 | And grab the bit out after the first sandbox. I then changed the final arg to `-fileno 1` and it printed the JSON. 200 | 201 | ```sh 202 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc --driver-mode=swift -L /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/4_2 -lPackageDescription -suppress-warnings -swift-version 4.2 -I /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk /Users/ortatherox/dev/projects/orta/ios/spm/PackageConfig/Package.swift -fileno 1 203 | 204 | {"errors": [], "package": {"cLanguageStandard": null, "cxxLanguageStandard": null, "dependencies": [], "name": "PackageConfig", "products": [{"name": "PackageConfig", "product_type": "library", "targets": ["PackageConfig"], "type": null}], "targets": [{"dependencies": [], "exclude": [], "name": "PackageConfig", "path": null, "publicHeadersPath": null, "sources": null, "type": "regular"}, {"dependencies": [{"name": "PackageConfig", "type": "byname"}], "exclude": [], "name": "PackageConfigTests", "path": null, "publicHeadersPath": null, "sources": null, "type": "test"}]}} 205 | ``` 206 | 207 | ### How I verify this works 208 | 209 | I run this command: 210 | 211 | ```sh 212 | swift build; env DEBUG="*" swift run package-config-example 213 | ``` 214 | 215 | if you don't use fish: 216 | 217 | ```sh 218 | swift build; DEBUG="*" swift run package-config-example 219 | ``` 220 | -------------------------------------------------------------------------------- /Sources/ExampleConfig/ExampleConfig.swift: -------------------------------------------------------------------------------- 1 | 2 | import protocol PackageConfig.PackageConfig 3 | 4 | public struct ExampleConfig: Codable, PackageConfig { 5 | 6 | let value: String 7 | 8 | public static var fileName: String = "example.config.json" 9 | 10 | public init(value: String) { 11 | self.value = value 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ExampleMain/main.swift: -------------------------------------------------------------------------------- 1 | import struct ExampleConfig.ExampleConfig 2 | 3 | do { 4 | let example = try ExampleConfig.load() 5 | print(example) 6 | } catch { 7 | print(error) 8 | } 9 | -------------------------------------------------------------------------------- /Sources/PackageConfig/DynamicLibraries.swift: -------------------------------------------------------------------------------- 1 | 2 | import class Foundation.Process 3 | import class Foundation.Pipe 4 | import class Foundation.NSRegularExpression 5 | import struct Foundation.NSRange 6 | 7 | enum DynamicLibraries { 8 | 9 | private static func read() -> [String] { 10 | let process = Process() 11 | let pipe = Pipe() 12 | 13 | process.launchPath = "/bin/bash" 14 | process.arguments = ["-c", "cat Package.swift"] 15 | process.standardOutput = pipe 16 | process.launch() 17 | process.waitUntilExit() 18 | 19 | return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)! 20 | .split(separator: "\n").map(String.init) 21 | } 22 | 23 | static func list() -> [String] { 24 | let lines = read() 25 | 26 | guard let start = lines.lastIndex(where: { $0.contains("PackageConfigs") }) else { 27 | return [] 28 | } 29 | 30 | let definition = lines.suffix(from: start) 31 | .joined(separator: "\n") 32 | .drop { !"[".contains($0) } 33 | .map(String.init) 34 | .joined() 35 | .split(separator: "\n") 36 | 37 | guard let end = definition.firstIndex(where: { $0.contains("]") }) else { 38 | return [] 39 | } 40 | 41 | return definition.prefix(end + 1) 42 | .reversed() 43 | .drop { "]".contains($0) } 44 | .reversed() 45 | .map(String.init) 46 | .map { 47 | guard let comment = $0.range(of: "//")?.lowerBound else { return $0 } 48 | return String($0[.. [String] { 62 | let lines = read() 63 | 64 | var matches: [String] = [] 65 | 66 | for line in lines { 67 | if let match = line.range(of: "import .*Config", options: .regularExpression) { 68 | matches.append(String(line[match])) 69 | } 70 | } 71 | 72 | debugLog("MATCHES: \(matches)") 73 | 74 | return matches 75 | .compactMap { $0.split(separator: " ") } 76 | .compactMap { $0.last } 77 | .map(String.init) 78 | .filter { !$0.contains("PackageDescription") } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/PackageConfig/Error.swift: -------------------------------------------------------------------------------- 1 | 2 | struct Error: Swift.Error, ExpressibleByStringLiteral { 3 | 4 | let reason: String 5 | 6 | init(_ reason: String) { 7 | self.reason = reason 8 | } 9 | 10 | init(stringLiteral reason: String) { 11 | self.reason = reason 12 | } 13 | 14 | var localizedDescription: String { 15 | return reason 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/PackageConfig/Loader.swift: -------------------------------------------------------------------------------- 1 | 2 | import func Foundation.NSTemporaryDirectory 3 | import class Foundation.FileManager 4 | import class Foundation.JSONDecoder 5 | 6 | enum Loader { 7 | static func load() throws -> T { 8 | let packageConfigJSON = NSTemporaryDirectory() + T.fileName 9 | 10 | guard let data = FileManager.default.contents(atPath: packageConfigJSON) else { 11 | throw Error("Could not find a file at \(packageConfigJSON) - something went wrong with compilation step probably") 12 | } 13 | 14 | return try JSONDecoder().decode(T.self, from: data) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/PackageConfig/Package.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Package { 4 | static func compile() throws { 5 | #if os(Linux) 6 | let swiftC = findPath(tool: "swiftc") 7 | #else 8 | let swiftC = try runXCRun(tool: "swiftc") 9 | #endif 10 | let process = Process() 11 | let linkedLibraries = try libraryLinkingArguments() 12 | var arguments = [String]() 13 | arguments += ["--driver-mode=swift"] // Eval in swift mode, I think? 14 | let swiftPMLib = getSwiftPMManifestArgs(swiftPath: swiftC) 15 | arguments += swiftPMLib // SwiftPM lib 16 | arguments += linkedLibraries 17 | arguments += ["-suppress-warnings"] // SPM does that too 18 | arguments += linkDynamicLibrary(path: ".build/debug") 19 | arguments += linkDynamicLibrary(path: swiftPMLib[1]) 20 | arguments += ["-sdk"] 21 | arguments += [findSDKPath()] // Add the SDK on which we need to compile into 22 | arguments += ["Package.swift"] // The Package.swift in the CWD 23 | 24 | // Create a process to eval the Swift Package manifest as a subprocess 25 | process.launchPath = swiftC 26 | process.arguments = arguments 27 | let stdPipe = Pipe() 28 | let errPipe = Pipe() 29 | process.standardOutput = stdPipe 30 | process.standardError = errPipe 31 | 32 | debugLog("CMD: \(swiftC) \(arguments.joined(separator: " "))") 33 | 34 | // Evaluation of the package swift code will end up 35 | // creating a file in the tmpdir that stores the JSON 36 | // settings when a new instance of PackageConfig is created 37 | process.launch() 38 | process.waitUntilExit() 39 | debugLog(String(data: stdPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!) 40 | debugLog(String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!) 41 | debugLog("Finished launching swiftc") 42 | } 43 | 44 | private static func linkDynamicLibrary(path: String) -> [String] { 45 | ["-Xlinker", "-rpath", "-Xlinker", path] 46 | } 47 | 48 | static func runIfNeeded() throws { 49 | let currentDirectory = FileManager.default.currentDirectoryPath 50 | 51 | if FileManager.default.fileExists(atPath: currentDirectory + "/Package") { 52 | debugLog("Running Package binary") 53 | let process = Process() 54 | let pipe = Pipe() 55 | 56 | process.launchPath = currentDirectory + "/Package" 57 | process.standardOutput = pipe 58 | 59 | process.launch() 60 | process.waitUntilExit() 61 | 62 | debugLog(String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!) 63 | 64 | try FileManager.default.removeItem(at: URL(fileURLWithPath: currentDirectory + "/Package")) 65 | } 66 | } 67 | 68 | static private func runXCRun(tool: String) throws -> String { 69 | let process = Process() 70 | let pipe = Pipe() 71 | 72 | process.launchPath = "/usr/bin/xcrun" 73 | process.arguments = ["--find", tool] 74 | process.standardOutput = pipe 75 | 76 | debugLog("CMD: \(process.launchPath!) \( ["--find", tool].joined(separator: " "))") 77 | 78 | process.launch() 79 | process.waitUntilExit() 80 | return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)! 81 | .trimmingCharacters(in: .whitespacesAndNewlines) 82 | } 83 | 84 | private static func findPath(tool: String) -> String { 85 | let process = Process() 86 | let pipe = Pipe() 87 | 88 | process.launchPath = "/bin/bash" 89 | process.arguments = ["-c", "command -v \(tool)"] 90 | process.standardOutput = pipe 91 | 92 | debugLog("CMD: \(process.launchPath!) \(process.arguments!.joined(separator: " "))") 93 | 94 | process.launch() 95 | process.waitUntilExit() 96 | return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)! 97 | .trimmingCharacters(in: .whitespacesAndNewlines) 98 | } 99 | 100 | private static func libraryPath(for library: String) -> String? { 101 | let fileManager = FileManager.default 102 | let libPaths = [ 103 | ".build/debug", 104 | ".build/x86_64-unknown-linux/debug", 105 | ".build/release", 106 | ] 107 | 108 | // "needs to be improved" 109 | // "consider adding `/usr/lib` to libPath maybe" 110 | 111 | func isLibPath(path: String) -> Bool { 112 | return fileManager.fileExists(atPath: path + "/lib\(library).dylib") || // macOS 113 | fileManager.fileExists(atPath: path + "/lib\(library).so") // Linux 114 | } 115 | 116 | return libPaths.first(where: isLibPath) 117 | } 118 | 119 | private static func libraryLinkingArguments() throws -> [String] { 120 | let packageConfigLib = "PackageConfig" 121 | guard let packageConfigPath = libraryPath(for: packageConfigLib) else { 122 | throw Error("PackageConfig: Could not find lib\(packageConfigLib) to link against, is it possible you've not built yet?") 123 | } 124 | let dyLibs = try DynamicLibraries.listImports().map { (libraryName: String) -> [String] in 125 | guard let path = libraryPath(for: libraryName) else { 126 | throw Error("PackageConfig: Could not find lib\(libraryName) to link against, is it possible you've not built yet?") 127 | } 128 | 129 | return [ 130 | "-L", path, 131 | "-I", path, 132 | "-l\(libraryName)", 133 | ] 134 | }.reduce([], +) 135 | 136 | debugLog("DYLIBS by IMPORT: \(dyLibs)") 137 | 138 | let configLibs = try DynamicLibraries.list().map { libraryName in 139 | guard let path = libraryPath(for: libraryName) else { 140 | throw Error("PackageConfig: Could not find lib\(libraryName) to link against, is it possible you've not built yet?") 141 | } 142 | 143 | return [ 144 | "-L", path, 145 | "-I", path, 146 | "-l\(libraryName)", 147 | ] 148 | }.reduce([ 149 | "-L", packageConfigPath, 150 | "-I", packageConfigPath, 151 | "-l\(packageConfigLib)", 152 | ], +) 153 | 154 | debugLog("CONFIG LIBS: \(configLibs)") 155 | 156 | return dyLibs + configLibs 157 | } 158 | 159 | private static func getSwiftPMManifestArgs(swiftPath: String) -> [String] { 160 | // using "xcrun --find swift" we get 161 | // /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc 162 | // we need to transform it to something like: 163 | // /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/4_2 164 | let fileManager = FileManager.default 165 | let swiftPMDir = swiftPath.replacingOccurrences(of: "bin/swiftc", with: "lib/swift/pm") 166 | let swiftToolsVersion = getSwiftToolsVersion() 167 | #if compiler(>=5.5.2) 168 | // Since Swift 5.5.2 there're no more different versions installed in the toolchain and it is stored 169 | // in another directory 170 | let directory = try! fileManager.contentsOfDirectory(atPath: swiftPMDir) 171 | .first(where: { $0.starts(with: "Manifest") }) 172 | let spmManifestDir = directory! 173 | #else 174 | let versions = try! fileManager.contentsOfDirectory(atPath: swiftPMDir) 175 | .filter { $0 != "llbuild" } 176 | .filter { $0.first?.isNumber ?? false } 177 | 178 | let latestVersion = versions.sorted().last! 179 | var spmManifestDir = latestVersion 180 | 181 | if let swiftToolsVersion = swiftToolsVersion, versions.contains(swiftToolsVersion) { 182 | spmManifestDir = swiftToolsVersion 183 | } 184 | #endif 185 | 186 | 187 | let packageDescriptionVersion = swiftToolsVersion?.replacingOccurrences(of: "_", with: ".") 188 | let libraryPathSPM = swiftPMDir + "/" + spmManifestDir 189 | 190 | debugLog("Using SPM version: \(libraryPathSPM)") 191 | return ["-L", libraryPathSPM, "-I", libraryPathSPM, "-lPackageDescription", "-package-description-version", packageDescriptionVersion ?? "5.2"] 192 | } 193 | 194 | private static func getSwiftToolsVersion() -> String? { 195 | guard let contents = try? String(contentsOfFile: "Package.swift") else { 196 | return nil 197 | } 198 | 199 | let range = NSRange(location: 0, length: contents.count) 200 | guard let regex = try? NSRegularExpression(pattern: "^// swift-tools-version:(?:(\\d)\\.(\\d)(?:\\.\\d)?)"), 201 | let match = regex.firstMatch(in: contents, options: [], range: range), 202 | let majorRange = Range(match.range(at: 1), in: contents), let major = Int(contents[majorRange]), 203 | let minorRange = Range(match.range(at: 2), in: contents), let minor = Int(contents[minorRange]) 204 | else { 205 | return nil 206 | } 207 | 208 | switch major { 209 | case 4: 210 | if minor < 2 { 211 | return "4" 212 | } 213 | return "4_2" 214 | default: 215 | return "\(major)_\(minor)" 216 | } 217 | } 218 | 219 | private static func findSDKPath() -> String { 220 | // xcrun --show-sdk-path 221 | let process = Process() 222 | let pipe = Pipe() 223 | 224 | process.launchPath = "/usr/bin/xcrun" 225 | process.arguments = ["--show-sdk-path"] 226 | process.standardOutput = pipe 227 | 228 | debugLog("CMD SDK path: \(process.launchPath!) \(process.arguments!.joined(separator: " "))") 229 | 230 | process.launch() 231 | process.waitUntilExit() 232 | return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)! 233 | .trimmingCharacters(in: .whitespacesAndNewlines) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Sources/PackageConfig/PackageConfig.swift: -------------------------------------------------------------------------------- 1 | 2 | public protocol PackageConfig: Codable { 3 | 4 | static var fileName: String { get } 5 | 6 | static func load() throws -> Self 7 | func write() 8 | } 9 | 10 | extension PackageConfig { 11 | 12 | public static func load() throws -> Self { 13 | try Package.compile() 14 | try Package.runIfNeeded() 15 | return try Loader.load() 16 | } 17 | 18 | public func write() { 19 | Writer.write(configuration: self) 20 | } 21 | } 22 | 23 | import class Foundation.ProcessInfo 24 | 25 | func debugLog(_ message: String) -> Void { 26 | let isVerbose = CommandLine.arguments.contains("--verbose") || (ProcessInfo.processInfo.environment["DEBUG"] != nil) 27 | if isVerbose { 28 | print(message) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PackageConfig/PackageConfiguration.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct PackageConfiguration: PackageConfig { 4 | public static var fileName: String = "package-config" 5 | 6 | public let configuration: [String: Any] 7 | 8 | public init(_ configuration: [String: Any]) { 9 | self.configuration = configuration 10 | } 11 | 12 | public subscript(string: String) -> Any? { 13 | return configuration[string] 14 | } 15 | 16 | public init(from decoder: Decoder) throws { 17 | let container = try decoder.singleValueContainer() 18 | let anyType = try container.decode(AnyType.self) 19 | 20 | configuration = try anyType.deserialise() 21 | } 22 | 23 | public func encode(to encoder: Encoder) throws { 24 | let anyType = configuration.mapValues(AnyType.init) 25 | 26 | var container = encoder.singleValueContainer() 27 | try container.encode(anyType) 28 | } 29 | } 30 | 31 | private struct AnyType: Codable { 32 | var jsonValue: Any 33 | 34 | init(_ jsonValue: Any) { 35 | if let value = jsonValue as? [String: Any] { 36 | self.jsonValue = value.mapValues(AnyType.init) 37 | } else if let value = jsonValue as? [Any] { 38 | self.jsonValue = value.map(AnyType.init) 39 | } else { 40 | self.jsonValue = jsonValue 41 | } 42 | } 43 | 44 | init(from decoder: Decoder) throws { 45 | let container = try decoder.singleValueContainer() 46 | 47 | if let intValue = try? container.decode(Int.self) { 48 | jsonValue = intValue 49 | } else if let stringValue = try? container.decode(String.self) { 50 | jsonValue = stringValue 51 | } else if let boolValue = try? container.decode(Bool.self) { 52 | jsonValue = boolValue 53 | } else if let doubleValue = try? container.decode(Double.self) { 54 | jsonValue = doubleValue 55 | } else if let doubleValue = try? container.decode(Array.self) { 56 | jsonValue = doubleValue 57 | } else if let doubleValue = try? container.decode(Dictionary.self) { 58 | jsonValue = doubleValue 59 | } else { 60 | throw DecodingError.typeMismatch(AnyType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unsupported type")) 61 | } 62 | } 63 | 64 | func encode(to encoder: Encoder) throws { 65 | var container = encoder.singleValueContainer() 66 | 67 | if let value = jsonValue as? Int { 68 | try container.encode(value) 69 | } else if let value = jsonValue as? String { 70 | try container.encode(value) 71 | } else if let value = jsonValue as? Bool { 72 | try container.encode(value) 73 | } else if let value = jsonValue as? Double { 74 | try container.encode(value) 75 | } else if let value = jsonValue as? [AnyType] { 76 | try container.encode(value) 77 | } else if let value = jsonValue as? [String: AnyType] { 78 | try container.encode(value) 79 | } else { 80 | throw DecodingError.typeMismatch(AnyType.self, DecodingError.Context(codingPath: encoder.codingPath, debugDescription: "Unsupported type")) 81 | } 82 | } 83 | 84 | func deserialise() throws -> [String: Any] { 85 | guard let result = deserialiseContent() as? [String: Any] else { 86 | throw DecodingError.typeMismatch(AnyType.self, DecodingError.Context(codingPath: [], debugDescription: "Expected a dictionary [String:Any]")) 87 | } 88 | 89 | return result 90 | } 91 | 92 | func deserialiseContent() -> Any { 93 | if let value = jsonValue as? [String: AnyType] { 94 | return value.mapValues { $0.deserialiseContent() } 95 | } else if let value = jsonValue as? [AnyType] { 96 | return value.map { $0.deserialiseContent() } 97 | } else { 98 | return jsonValue 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/PackageConfig/Writer.swift: -------------------------------------------------------------------------------- 1 | 2 | import class Foundation.JSONEncoder 3 | import class Foundation.FileManager 4 | import func Foundation.NSTemporaryDirectory 5 | 6 | enum Writer { 7 | 8 | static func write(configuration: T) { 9 | let packageConfigJSON = NSTemporaryDirectory() + T.fileName 10 | let encoder = JSONEncoder() 11 | 12 | do { 13 | let data = try encoder.encode(configuration) 14 | 15 | if !FileManager.default.createFile(atPath: packageConfigJSON, contents: data, attributes: nil) { 16 | debugLog("PackageConfig: Could not create a temporary file for the PackageConfig: \(packageConfigJSON)") 17 | } 18 | } catch { 19 | debugLog("Package config failed to encode configuration \(configuration)") 20 | } 21 | 22 | debugLog("written to path: \(packageConfigJSON)") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PackageConfigExecutable/main.swift: -------------------------------------------------------------------------------- 1 | 2 | import class Foundation.Process 3 | 4 | let process = Process() 5 | let script = 6 | """ 7 | mkdir -p ./Sources/PackageConfigs/ 8 | touch ./Sources/PackageConfigs/PackageConfigs.swift 9 | swift package resolve 10 | swift build --target PackageConfigs 11 | """ 12 | process.launchPath = "/bin/bash" 13 | process.arguments = ["-c", script] 14 | process.launch() 15 | process.waitUntilExit() 16 | -------------------------------------------------------------------------------- /Sources/PackageConfigs/PackageConfigs.swift: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shibapm/PackageConfig/58523193c26fb821ed1720dcd8a21009055c7cdb/Sources/PackageConfigs/PackageConfigs.swift --------------------------------------------------------------------------------