Created
September 5, 2018 14:38
-
-
Save alexisakers/ec0b605a1c710e7506dd22d5470a4dc0 to your computer and use it in GitHub Desktop.
Swift Script to update Carthage dependencies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// update_carthage_dependencies | |
// Copyright (C) 2018 Alexis Aubry | |
// | |
// -------------------------------------------------------------------- | |
// This script updates your Licenses plist file with the | |
// latest licenses and dependencies from Carthage. | |
// | |
// In Xcode, add this script as a build phase, before the "Copy Bundle | |
// Resources" phase in the main target. | |
// | |
// The first input file must be the Cartfile.resolved file. The second | |
// input file must be the Cartfile/Checkouts directory. The output file | |
// must be the plist file that contains the licenses. | |
// | |
// This script will be run everytime we clean the project, and when the | |
// build enviroment changes (when we update Carthage or add/remove | |
// dependencies). | |
// | |
import Foundation | |
// MARK: - Models | |
/// The structure representing license items to add in the Plist. | |
struct Dependency: Encodable { | |
/// The name of the project. | |
let name: String | |
/// The text of the license file. | |
let licenseText: String | |
/// The URL of the project. | |
let projectURL: String | |
enum CodingKeys: String, CodingKey { | |
case name = "Name" | |
case licenseText = "LicenseText" | |
case projectURL = "ProjectURL" | |
} | |
} | |
/// The different file names for the license files. | |
let licenseSpellings = [ | |
"LICENSE", | |
"License", | |
"LICENSE.txt", | |
"License.txt", | |
"LICENSE.md", | |
"License.md" | |
] | |
// MARK: - Helpers | |
/// Exits the script because of an error. | |
func fail(_ error: String) -> Never { | |
print("💥 \(error)") | |
exit(-1) | |
} | |
/// Prints an info message. | |
func info(_ message: String) { | |
print("ℹ️ \(message)") | |
} | |
/// Prints a success message and exits the script. | |
func success(_ message: String) -> Never { | |
print("✅ \(message)") | |
exit(0) | |
} | |
/// Gets an environment variable value with the given name. | |
func getEnvironmentValue(key: String) -> String? { | |
guard let rawValue = getenv(key) else { | |
return nil | |
} | |
return String(cString: rawValue) | |
} | |
extension String { | |
/// Removes the license columns in the string. | |
var removingColumns: String { | |
let paragraphs = self.components(separatedBy: "\n\n") | |
var singleLines: [String] = [] | |
for paragraph in paragraphs { | |
let lines = paragraph.components(separatedBy: "\n") | |
let sanitizedLines = lines.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } | |
let singleLine = sanitizedLines.joined(separator: " ") | |
singleLines.append(singleLine) | |
} | |
return singleLines.joined(separator: "\n\n") | |
} | |
} | |
// MARK: - Arguments | |
/// Returns the input files. | |
func getInputs() -> (cartfile: URL, checkouts: URL) { | |
guard let cartfilePath = getEnvironmentValue(key: "SCRIPT_INPUT_FILE_0") else { | |
fail("The first input file in Xcode must be the 'Cartfile.resolved' file.") | |
} | |
guard let checkoutsPath = getEnvironmentValue(key: "SCRIPT_INPUT_FILE_1") else { | |
fail("The second input file in Xcode must be the 'Cartfile/Checkouts' folder.") | |
} | |
return (URL(fileURLWithPath: cartfilePath), URL(fileURLWithPath: checkoutsPath)) | |
} | |
/// Returns the output file. | |
func getOutput() -> URL { | |
guard let plistPath = getEnvironmentValue(key: "SCRIPT_OUTPUT_FILE_0") else { | |
fail("The second input file in Xcode must be the 'Cartfile/Checkouts' folder.") | |
} | |
return URL(fileURLWithPath: plistPath) | |
} | |
/// Gets the license text in the given directory. | |
func getLicenseURL(in directory: URL) -> URL? { | |
guard let topLevelItems = try? FileManager.default.contentsOfDirectory(atPath: directory.path) else { | |
print("NO TLI \(directory.path)") | |
return nil | |
} | |
for spelling in licenseSpellings { | |
if topLevelItems.contains(spelling) { | |
let possibleURL = directory.appendingPathComponent(spelling) | |
guard FileManager.default.fileExists(atPath: possibleURL.path) else { | |
continue | |
} | |
return possibleURL | |
} | |
} | |
return nil | |
} | |
// MARK: - Parsing | |
/// Get the list of depedencies from the Cartfile.resolved file. | |
func generateFromCartfileResolved(_ content: String, checkoutsDir: URL) -> [Dependency] { | |
let dependencyLines = content.split(separator: "\n").filter { $0.hasPrefix("github") } | |
var items: [Dependency] = [] | |
for dependency in dependencyLines { | |
// 1) Parse the component info from the Carthage file (ex: github "wireapp/dependency" "version") | |
let components = dependency.components(separatedBy: " ") | |
guard components.count == 3 else { | |
info("Skipping invalid dependency.") | |
continue | |
} | |
let projectPath = components[1].trimmingCharacters(in: .punctuationCharacters) | |
let url = URL(string: "https://github.com/\(projectPath)")! | |
let projectComponents = projectPath.components(separatedBy: "/") | |
guard projectComponents.count == 2 else { | |
info("Skipping invalid dependency.") | |
continue | |
} | |
let name = projectComponents[1].trimmingCharacters(in: .symbols) | |
// 2) Get the license text from the checkout out directory | |
let projectCheckoutFolder = checkoutsDir.appendingPathComponent(name, isDirectory: true) | |
guard let licenseTextURL = getLicenseURL(in: projectCheckoutFolder) else { | |
info("The dependency \(name) does not have a license. Skipping.") | |
continue | |
} | |
guard let data = try? Data(contentsOf: licenseTextURL) else { | |
info("Could not read the license of \(name). Skipping.") | |
break | |
} | |
let licenseText = String(decoding: data, as: UTF8.self).removingColumns | |
//3) Create the item | |
let item = Dependency(name: name, licenseText: licenseText, projectURL: url.absoluteString) | |
items.append(item) | |
} | |
return items.sorted { $0.name < $1.name } | |
} | |
// MARK: - Execution | |
let (cartfileURL, checkoutsURL) = getInputs() | |
let outputURL = getOutput() | |
// 1) Decode the Cartfile | |
let cartfileBinary = try Data(contentsOf: cartfileURL) | |
let cartfileContents = String(decoding: cartfileBinary, as: UTF8.self) | |
let items = generateFromCartfileResolved(cartfileContents, checkoutsDir: checkoutsURL) | |
info("Found \(items.count) dependencies.") | |
let encoder = PropertyListEncoder() | |
encoder.outputFormat = .binary | |
// 2) Encode and write the data | |
let encodedPlist = try encoder.encode(items) | |
try? FileManager.default.removeItem(at: outputURL) | |
try encodedPlist.write(to: outputURL) | |
success("Successfully updated the list of licenses") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment