├── .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
--------------------------------------------------------------------------------