diff --git a/Package.swift b/Package.swift index b740e07..9617e5c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:4.1 +// swift-tools-version:4.2 /** * Marathon diff --git a/Sources/MarathonCore/Dependency.swift b/Sources/MarathonCore/Dependency.swift new file mode 100644 index 0000000..88a2556 --- /dev/null +++ b/Sources/MarathonCore/Dependency.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct Dependency { + public let name: String? + public var url: URL + + public init(name: String? = nil, url: URL) { + self.name = name + self.url = url + } +} + +extension Dependency: Equatable { + public static func ==(lhs: Dependency, rhs: Dependency) -> Bool { + return + lhs.name == rhs.name + && lhs.url == rhs.url + } +} diff --git a/Sources/MarathonCore/Package.swift b/Sources/MarathonCore/Package.swift index ba4d0ca..ddf5be8 100644 --- a/Sources/MarathonCore/Package.swift +++ b/Sources/MarathonCore/Package.swift @@ -11,12 +11,19 @@ import Releases public struct Package { public let name: String public let url: URL - public var majorVersion: Int + public var version: Version + public var majorVersion: Int { return version.major } } extension Package: Equatable { public static func ==(lhs: Package, rhs: Package) -> Bool { - return lhs.url == rhs.url && lhs.majorVersion == rhs.majorVersion + return lhs.url == rhs.url && lhs.version == rhs.version + } +} + +extension Package: Hashable { + public var hashValue: Int { + return "\(url.absoluteString);\(version.description)".hashValue } } @@ -24,7 +31,17 @@ extension Package: Unboxable { public init(unboxer: Unboxer) throws { name = try unboxer.unbox(key: "name") url = try unboxer.unbox(key: "url") - majorVersion = try unboxer.unbox(key: "majorVersion") + + if let versionData: [String: Int] = try? unboxer.unbox(key: "version") + , let major: Int = versionData["major"] + , let minor: Int = versionData["minor"] + , let patch: Int = versionData["patch"] { + version = Version(major: major, minor: minor, patch: patch, prefix: nil, suffix: nil) + } else { + let majorVersion: Int = try unboxer.unbox(key: "majorVersion") + version = Version(major: majorVersion) + } + } } @@ -41,8 +58,12 @@ internal extension Package { if toolsVersion.major == 3 { return ".Package(url: \"\(url.absoluteString)\", majorVersion: \(majorVersion))" } + + if toolsVersion >= Version(major: 4, minor: 2) && !url.isForRemoteRepository { + return ".package(path: \"\(url.absoluteString)\")" + } - return ".package(url: \"\(url.absoluteString)\", from: \"\(majorVersion).0.0\")" + return ".package(url: \"\(url.absoluteString)\", from: \"\(version.major).\(version.minor).\(version.patch)\")" } } diff --git a/Sources/MarathonCore/PackageManager.swift b/Sources/MarathonCore/PackageManager.swift index bfb51b8..652c4cd 100644 --- a/Sources/MarathonCore/PackageManager.swift +++ b/Sources/MarathonCore/PackageManager.swift @@ -114,8 +114,8 @@ public final class PackageManager { } } - let latestVersion = try latestMajorVersionForPackage(at: url) - let package = Package(name: name, url: absoluteRepositoryURL(from: url), majorVersion: latestVersion) + let latestVersion = try latestVersionForPackage(at: url) + let package = Package(name: name, url: absoluteRepositoryURL(from: url), version: latestVersion) try save(package: package) try updatePackages() @@ -124,17 +124,21 @@ public final class PackageManager { return package } - public func addPackagesIfNeeded(from packageURLs: [URL]) throws { - let existingPackageURLs = Set(makePackageList().map { package in - return package.url.absoluteString.lowercased() - }) + public func addPackagesIfNeeded(from dependencies: [Dependency]) throws -> [Dependency] { + let existingPackages = makePackageList() - for url in packageURLs { - guard !existingPackageURLs.contains(url.absoluteString.lowercased()) else { - continue + return try dependencies.map { dependency throws in + let existingPackage = try existingPackages.first { (package) throws -> Bool in + return package.url.absoluteString.lowercased() == dependency.url.absoluteString.lowercased() + } + guard existingPackage == nil else { + return dependency } - try addPackage(at: url, throwIfAlreadyAdded: false) + let newPackage = try addPackage(at: dependency.url, throwIfAlreadyAdded: false) + var dep = dependency + dep.url = newPackage.url // make sure that any ~ or trailing slashes are identical by setting a new URL on the dependency + return dep } } @@ -165,20 +169,10 @@ public final class PackageManager { } public func makePackageDescription(for script: Script) throws -> String { - guard let masterDescription = try? generatedFolder.file(named: "Package.swift").readAsString() else { - try updatePackages() - return try makePackageDescription(for: script) - } - let toolsVersion = try resolveSwiftToolsVersion() let expectedHeader = makePackageDescriptionHeader(forSwiftToolsVersion: toolsVersion) - guard masterDescription.hasPrefix(expectedHeader) else { - try generateMasterPackageDescription(forSwiftToolsVersion: toolsVersion) - return try makePackageDescription(for: script) - } - - return masterDescription.replacingOccurrences(of: masterPackageName, with: script.name) + return try generatePackageDescription(for: script, toolsVersion: toolsVersion) } public func symlinkPackages(to folder: Folder) throws { @@ -219,13 +213,13 @@ public final class PackageManager { public func updateAllPackagesToLatestMajorVersion() throws { for var package in addedPackages { - let latestMajorVersion = try latestMajorVersionForPackage(at: package.url) + let latestVersion = try latestVersionForPackage(at: package.url) - guard latestMajorVersion > package.majorVersion else { + guard latestVersion > package.version else { continue } - package.majorVersion = latestMajorVersion + package.version = latestVersion try save(package: package) } @@ -257,7 +251,7 @@ public final class PackageManager { // MARK: - Private - private func latestMajorVersionForPackage(at url: URL) throws -> Int { + private func latestVersionForPackage(at url: URL) throws -> Version { printer.reportProgress("Resolving latest major version for \(url.absoluteString)...") let releases = try perform(Releases.versions(for: url).withoutPreReleases(), @@ -267,7 +261,7 @@ public final class PackageManager { throw Error.failedToResolveLatestVersion(url) } - return latestVersion.major + return latestVersion } private func nameOfPackage(at url: URL) throws -> String { @@ -315,14 +309,14 @@ public final class PackageManager { private func updatePackages() throws { printer.reportProgress("Updating packages...") - do { - let toolsVersion = try resolveSwiftToolsVersion() - try generateMasterPackageDescription(forSwiftToolsVersion: toolsVersion) - try shellOutToSwiftCommand("package update", in: generatedFolder, printer: printer) - try generatedFolder.createSubfolderIfNeeded(withName: "Packages") - } catch { - throw Error.failedToUpdatePackages(folder) - } +// do { +// let toolsVersion = try resolveSwiftToolsVersion() +// try generateMasterPackageDescription(forSwiftToolsVersion: toolsVersion) +// try shellOutToSwiftCommand("package update", in: generatedFolder, printer: printer) +// try generatedFolder.createSubfolderIfNeeded(withName: "Packages") +// } catch { +// throw Error.failedToUpdatePackages(folder) +// } } private func addMissingPackageFiles() { @@ -335,7 +329,7 @@ public final class PackageManager { let package = Package( name: pinnedPackage.name, url: pinnedPackage.url, - majorVersion: pinnedPackage.version.major + version: pinnedPackage.version ) try save(package: package) @@ -399,12 +393,69 @@ public final class PackageManager { try generatedFolder.createFile(named: "Package.swift", contents: description.data(using: .utf8).require()) } + + private func generatePackageDescription(for script: Script, toolsVersion: Version) throws -> String{ + let header = makePackageDescriptionHeader(forSwiftToolsVersion: toolsVersion) + let packages = makePackageDictionary() + + var description = "\(header)\n\n" + + "import PackageDescription\n\n" + + "let package = Package(\n" + + " name: \"\(script.name)\",\n" + + " products: [],\n" + + " dependencies: [\n" + + let dependencyPackages = try Set(script.dependencies.map { dependency throws -> Package in + guard let package = packages[dependency.url] else { + throw PackageManagerError.failedToReadPackageFile(dependency.url.absoluteString) + } + return package + }) + + for (index, package) in dependencyPackages.enumerated() { + if index > 0 { + description += ",\n" + } + + let dependencyString = package.dependencyString(forSwiftToolsVersion: toolsVersion) + description.append(" \(dependencyString)") + } + + description.append("\n ],\n") + + if toolsVersion.major > 3 { + description.append(" targets: [.target(name: \"\(script.name)\", dependencies: [") + + if !script.dependencies.isEmpty { + description.append("\"") + let dependencyNames = try script.dependencies.map { try $0.name ?? nameOfPackage(at: $0.url) } + description.append(dependencyNames.joined(separator: "\", \"")) + description.append("\"") + } + + description.append("])],\n") + } + + if toolsVersion.major >= 4 && toolsVersion.minor >= 2 { + description.append(" swiftLanguageVersions: [.version(\"\(toolsVersion.major).\(toolsVersion.minor)\")]\n)") + } else { + description.append(" swiftLanguageVersions: [\(toolsVersion.major)]\n)") + } + + return description + } private func makePackageList() -> [Package] { return folder.files.compactMap { file in return try? unbox(data: file.read()) } } + + private func makePackageDictionary() -> [URL: Package] { + var packageDict: [URL: Package] = [:] + makePackageList().forEach { packageDict[$0.url] = $0 } + return packageDict + } private func resolveSwiftToolsVersion() throws -> Version { var versionString = try shellOutToSwiftCommand("package --version", printer: printer) diff --git a/Sources/MarathonCore/Script.swift b/Sources/MarathonCore/Script.swift index a264677..2651dd0 100644 --- a/Sources/MarathonCore/Script.swift +++ b/Sources/MarathonCore/Script.swift @@ -69,6 +69,7 @@ public final class Script { public let name: String public let folder: Folder + public var dependencies: [Dependency] private let printer: Printer private var copyLoopDispatchQueue: DispatchQueue? @@ -76,9 +77,10 @@ public final class Script { // MARK: - Init - init(name: String, folder: Folder, printer: Printer) { + public init(name: String, folder: Folder, dependencies: [Dependency], printer: Printer) { self.name = name self.folder = folder + self.dependencies = dependencies self.printer = printer } diff --git a/Sources/MarathonCore/ScriptManager.swift b/Sources/MarathonCore/ScriptManager.swift index 58e1d1a..0880110 100644 --- a/Sources/MarathonCore/ScriptManager.swift +++ b/Sources/MarathonCore/ScriptManager.swift @@ -169,14 +169,16 @@ public final class ScriptManager { private func script(from file: File) throws -> Script { let identifier = scriptIdentifier(from: file.path) let folder = try createFolderIfNeededForScript(withIdentifier: identifier, file: file) - let script = Script(name: file.nameExcludingExtension, folder: folder, printer: printer) - + let script = Script(name: file.nameExcludingExtension, folder: folder, dependencies: [], printer: printer) + if let marathonFile = try script.resolveMarathonFile(fileName: config.dependencyFile) { - try packageManager.addPackagesIfNeeded(from: marathonFile.packageURLs) + let marathonFileDependencies: [Dependency] = marathonFile.packageURLs.map { Dependency(name: nil, url: $0) } + let resolvedDependencies = try packageManager.addPackagesIfNeeded(from: marathonFileDependencies) try addDependencyScripts(fromMarathonFile: marathonFile, for: script) + script.dependencies += resolvedDependencies } - try resolveInlineDependencies(from: file) + script.dependencies += try resolveInlineDependencies(from: file) do { let packageFile = try folder.createFile(named: "Package.swift") @@ -248,7 +250,7 @@ public final class ScriptManager { let cloneFiles = cloneFolder.makeFileSequence(recursive: true) if cloneFiles.contains(where: { $0.name == "main.swift" }) { - return Script(name: packageName, folder: cloneFolder, printer: printer) + return Script(name: packageName, folder: cloneFolder, dependencies: [], printer: printer) } } @@ -278,7 +280,7 @@ public final class ScriptManager { private func createFolderIfNeededForScript(withIdentifier identifier: String, file: File) throws -> Folder { let scriptFolder = try cacheFolder.createSubfolderIfNeeded(withName: identifier) - try packageManager.symlinkPackages(to: scriptFolder) +// try packageManager.symlinkPackages(to: scriptFolder) if (try? scriptFolder.file(named: "OriginalFile")) == nil { try scriptFolder.createSymlink(to: file.path, at: "OriginalFile", printer: printer) @@ -310,9 +312,10 @@ public final class ScriptManager { } } - private func resolveInlineDependencies(from file: File) throws { + private func resolveInlineDependencies(from file: File) throws -> [Dependency] { let lines = try file.readAsString().components(separatedBy: .newlines) - var packageURLs = [URL]() + + var dependencies = [Dependency]() for line in lines { if line.hasPrefix("import ") { @@ -322,13 +325,17 @@ public final class ScriptManager { continue } + let importName = components.first! + .replacingOccurrences(of: "import ", with: "") + .replacingOccurrences(of: "//", with: "") + .trimmingCharacters(in: .whitespaces) let urlString = components.last!.trimmingCharacters(in: .whitespaces) guard let url = URL(string: urlString) else { throw Error.invalidInlineDependencyURL(urlString) } - packageURLs.append(url) + dependencies.append(Dependency(name: importName, url: url)) } else if let firstCharacter = line.unicodeScalars.first { guard !CharacterSet.alphanumerics.contains(firstCharacter) else { break @@ -336,7 +343,7 @@ public final class ScriptManager { } } - try packageManager.addPackagesIfNeeded(from: packageURLs) + return try packageManager.addPackagesIfNeeded(from: dependencies) } private func makeManagedScriptPathList() -> [String] {