├── Sources ├── SwiftbrewCore │ ├── Version.swift │ ├── Logger.swift │ ├── PackageReference+Extension.swift │ ├── Platforms.swift │ └── SwiftbrewCore.swift └── Swiftbrew │ ├── main.swift │ ├── uninstall.swift │ ├── root.swift │ └── install.swift ├── .gitignore ├── .travis.yml ├── Tests ├── LinuxMain.swift └── SwiftbrewTests │ ├── XCTestManifests.swift │ └── SwiftbrewTests.swift ├── .github └── workflows │ └── pull-request-workflow.yml ├── RELEASING.md ├── LICENSE ├── Package.swift ├── Makefile ├── README.md └── Package.resolved /Sources/SwiftbrewCore/Version.swift: -------------------------------------------------------------------------------- 1 | public let version = "0.1.1" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | .swiftpm 6 | *.tar.xz 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode10.2 3 | script: 4 | - swift test 5 | -------------------------------------------------------------------------------- /Sources/Swiftbrew/main.swift: -------------------------------------------------------------------------------- 1 | rootCommand.add(subCommand: installCommand) 2 | rootCommand.add(subCommand: uninstallCommand) 3 | rootCommand.execute() 4 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SwiftbrewTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SwiftbrewTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/SwiftbrewTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SwiftbrewTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-workflow.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: macOS-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Build 10 | run: swift build -v 11 | - name: Run tests 12 | run: swift test -v 13 | -------------------------------------------------------------------------------- /Sources/SwiftbrewCore/Logger.swift: -------------------------------------------------------------------------------- 1 | import Colorizer 2 | 3 | func printProcessingInfo(_ message: String) { 4 | print("==> ".foreground.Cyan + message.style.Bold) 5 | } 6 | 7 | func printInfo(_ message: String) { 8 | print(message) 9 | } 10 | 11 | func printWarning(_ message: String) { 12 | print("Warning: ".foreground.Yellow + message) 13 | } 14 | 15 | func printError(_ message: String) { 16 | print("Error: ".foreground.Red + message) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/SwiftbrewCore/PackageReference+Extension.swift: -------------------------------------------------------------------------------- 1 | import MintKit 2 | 3 | extension PackageReference { 4 | var repoPath: String { 5 | return gitPath 6 | .components(separatedBy: "://").last! 7 | .replacingOccurrences(of: "/", with: "_") 8 | .replacingOccurrences(of: ".git", with: "") 9 | .replacingOccurrences(of: ":", with: "_") 10 | .replacingOccurrences(of: "@", with: "_") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | 1. Make sure you have both clones of 2 | [swiftbrew/Swiftbrew](https://github.com/swiftbrew/Swiftbrew) and 3 | [swiftbrew/homebrew-tap](https://github.com/swiftbrew/homebrew-tap) under a 4 | same parent directory, for example: 5 | 6 | ``` 7 | swiftbrew-org 8 | ├── Swiftbrew 9 | └── homebrew-tap 10 | ``` 11 | 12 | 2. Make sure you have [hub](https://hub.github.com) installed and configured. 13 | 3. Run `make release version=x.y.z` to release a new version `x.y.z`. 14 | -------------------------------------------------------------------------------- /Sources/Swiftbrew/uninstall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Guaka 3 | import Just 4 | import SwiftbrewCore 5 | 6 | var uninstallCommand = Command( 7 | usage: "uninstall", 8 | configuration: configuration, 9 | run: run 10 | ) 11 | 12 | private func configuration(command: Command) { 13 | command.shortMessage = "Uninstall a package" 14 | command.longMessage = """ 15 | Uninstall a Swift command line tool package by name: 16 | 17 | swift brew uninstall 18 | """ 19 | command.example = """ 20 | swift brew uninstall xcbeautify 21 | """ 22 | } 23 | 24 | private func run(flags: Flags, args: [String]) { 25 | guard let package = args.first else { 26 | // Print help 27 | return 28 | } 29 | 30 | try? SwiftbrewCore.uninstall(name: package) 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Swiftbrew/root.swift: -------------------------------------------------------------------------------- 1 | import Guaka 2 | import SwiftbrewCore 3 | 4 | var rootCommand = Command( 5 | usage: "swift-brew", 6 | configuration: configuration, 7 | run: nil 8 | ) 9 | 10 | private func configuration(command: Command) { 11 | command.add(flags: [ 12 | .init(shortName: "v", 13 | longName: "version", 14 | value: false, 15 | description: "Print the version", 16 | inheritable: true) 17 | ]) 18 | 19 | command.inheritablePreRun = { flags, args in 20 | if let versionFlag = flags.getBool(name: "version"), versionFlag == true { 21 | print(SwiftbrewCore.version) 22 | return false 23 | } 24 | 25 | return true 26 | } 27 | } 28 | 29 | private func run(flags: Flags, args: [String]) { 30 | } 31 | -------------------------------------------------------------------------------- /Sources/SwiftbrewCore/Platforms.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct MacOS { 4 | let majorVersion: Int 5 | let minorVersion: Int 6 | 7 | func name() -> String { 8 | guard majorVersion == 10 else { 9 | fatalError("Unsupported macOS version") 10 | } 11 | 12 | switch minorVersion { 13 | case 13: 14 | return "high_sierra" 15 | case 14: 16 | return "mojave" 17 | case 15: 18 | // Install bottles for macOS Mojave for now until we have build workers for macOS Catalina. 19 | // return "catalina" 20 | return "mojave" 21 | default: 22 | return "macos_10.16" 23 | } 24 | } 25 | } 26 | 27 | func currentPlatformName() -> String { 28 | #if os(macOS) 29 | let osVersion = ProcessInfo.processInfo.operatingSystemVersion 30 | 31 | return MacOS( 32 | majorVersion: osVersion.majorVersion, 33 | minorVersion: osVersion.minorVersion) 34 | .name() 35 | #else 36 | return "x86_64_linux" 37 | #endif 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Thi Doãn 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Sources/Swiftbrew/install.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Guaka 3 | import Just 4 | import SwiftbrewCore 5 | 6 | var installCommand = Command( 7 | usage: "install", 8 | configuration: configuration, 9 | run: run 10 | ) 11 | 12 | private func configuration(command: Command) { 13 | command.shortMessage = "Install a package" 14 | command.longMessage = """ 15 | Install a Swift command line tool package: 16 | 17 | swift brew install 18 | 19 | can be a shorthand for a GitHub repository (Carthage/Carthage) 20 | or a full git URL (https://github.com/Carthage/Carthage.git), optionally followed 21 | by a tagged version (@x.y.z). 22 | 23 | Note: Swiftbrew currently only supports public repositories. 24 | """ 25 | command.example = """ 26 | swift brew install thii/xcbeautify 27 | swift brew install thii/xcbeautify@0.4.3 28 | swift brew install https://github.com/thii/xcbeautify 29 | """ 30 | } 31 | 32 | private func run(flags: Flags, args: [String]) { 33 | guard let package = args.first else { 34 | // Print help 35 | return 36 | } 37 | 38 | try? SwiftbrewCore.install(package: package) 39 | } 40 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Swiftbrew", 7 | platforms: [ 8 | .macOS(.v10_10) 9 | ], 10 | products: [ 11 | .executable(name: "swift-brew", targets: ["Swiftbrew"]) 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/JustHTTP/Just.git", .upToNextMajor(from: "0.7.0")), 15 | .package(url: "https://github.com/getGuaka/Colorizer.git", .upToNextMajor(from: "0.2.1")), 16 | .package(url: "https://github.com/getGuaka/Run.git", .upToNextMajor(from: "0.1.1")), 17 | .package(url: "https://github.com/nsomar/Guaka.git", .upToNextMajor(from: "0.4.1")), 18 | .package(url: "https://github.com/yonaskolb/Mint.git", .upToNextMajor(from: "0.12.0")), 19 | 20 | // SwiftCLI 5.3.0 doesn't build. This locks it to 5.2.x. 21 | // This is a transitive dependency of Mint. We're not using this. 22 | .package(url: "https://github.com/jakeheis/SwiftCLI.git", .upToNextMinor(from: "5.2.2")), 23 | ], 24 | targets: [ 25 | .target( 26 | name: "Swiftbrew", 27 | dependencies: ["Guaka", "SwiftbrewCore"]), 28 | .target( 29 | name: "SwiftbrewCore", 30 | dependencies: ["Colorizer", "Just", "MintKit", "Run"]), 31 | .testTarget( 32 | name: "SwiftbrewTests", 33 | dependencies: ["Swiftbrew"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /Tests/SwiftbrewTests/SwiftbrewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.Bundle 3 | @testable import SwiftbrewCore 4 | 5 | final class SwiftbrewTests: XCTestCase { 6 | func testVersion() throws { 7 | // Some of the APIs that we use below are available in macOS 10.13 and above. 8 | guard #available(macOS 10.13, *) else { 9 | return 10 | } 11 | 12 | let fooBinary = productsDirectory.appendingPathComponent("swift-brew") 13 | 14 | let process = Process() 15 | process.executableURL = fooBinary 16 | process.arguments = ["--version"] 17 | 18 | let pipe = Pipe() 19 | process.standardOutput = pipe 20 | 21 | try process.run() 22 | process.waitUntilExit() 23 | 24 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 25 | let output = String(data: data, encoding: .utf8) 26 | 27 | let expectedOutput = SwiftbrewCore.version 28 | XCTAssertEqual(output, "\(expectedOutput)\n") 29 | } 30 | 31 | /// Returns path to the built products directory. 32 | var productsDirectory: URL { 33 | #if os(macOS) 34 | for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { 35 | return bundle.bundleURL.deletingLastPathComponent() 36 | } 37 | fatalError("couldn't find the products directory") 38 | #else 39 | return Bundle.main.bundleURL 40 | #endif 41 | } 42 | 43 | static var allTests = [ 44 | ("testVersion", testVersion), 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PRODUCT_NAME = swift-brew 2 | VERSION = $(version) 3 | 4 | PREFIX ?= /usr/local 5 | 6 | CD = cd 7 | CP = $(shell whereis cp) -Rf 8 | GIT = $(shell which git) 9 | HUB = $(shell which hub) 10 | MKDIR = $(shell which mkdir) -p 11 | RM = $(shell whereis rm) -rf 12 | SED = /usr/bin/sed 13 | SWIFT = $(shell which swift) 14 | TAR = $(shell whereis tar) cJf 15 | 16 | TARGET_PLATFORM = x86_64-apple-macosx 17 | TARBALL = "swiftbrew-$(VERSION).mojave.tar.xz" 18 | 19 | BINARY_DIRECTORY = $(PREFIX)/bin 20 | BUILD_DIRECTORY = $(shell pwd)/.build/$(TARGET_PLATFORM)/release 21 | OUTPUT_EXECUTABLE = $(BUILD_DIRECTORY)/$(PRODUCT_NAME) 22 | INSTALL_EXECUTABLE_PATH = $(BINARY_DIRECTORY)/$(PRODUCT_NAME) 23 | 24 | SWIFT_BUILD_FLAGS = --configuration release --disable-sandbox 25 | 26 | .PHONY: all 27 | all: build 28 | 29 | .PHONY: test 30 | test: clean 31 | $(SWIFT) test 32 | 33 | .PHONY: build 34 | build: 35 | $(SWIFT) build $(SWIFT_BUILD_FLAGS) 36 | 37 | .PHONY: install 38 | install: build 39 | $(MKDIR) $(BINARY_DIRECTORY) 40 | $(CP) "$(OUTPUT_EXECUTABLE)" "$(BINARY_DIRECTORY)" 41 | 42 | .PHONY: package 43 | package: build 44 | $(CD) "$(BUILD_DIRECTORY)" && $(TAR) $(TARBALL) "$(PRODUCT_NAME)" 45 | $(CP) "$(BUILD_DIRECTORY)/$(TARBALL)" $(TARBALL) 46 | 47 | .PHONY: release 48 | release: clean package 49 | $(GIT) --git-dir=../homebrew-tap/.git pull origin master 50 | $(SED) -i '' '4s/.*/ version "$(VERSION)"/' ../homebrew-tap/swiftbrew.rb 51 | $(SED) -i '' '6s/.*/ sha256 "$(shell shasum -a 256 "$(TARBALL)" | cut -f 1 -d " ")"/' ../homebrew-tap/swiftbrew.rb 52 | $(HUB) release create --message $(VERSION) --attach $(TARBALL) $(VERSION) 53 | $(CD) ../homebrew-tap && \ 54 | $(GIT) commit swiftbrew.rb -m "Release version $(VERSION)" 55 | $(GIT) --git-dir=../homebrew-tap/.git push origin master 56 | 57 | .PHONY: xcode 58 | xcode: 59 | $(SWIFT) package generate-xcodeproj 60 | 61 | .PHONY: uninstall 62 | uninstall: 63 | $(RM) "$(BINARY_DIRECTORY)/$(PRODUCT_NAME)" 64 | 65 | .PHONY: clean 66 | clean: 67 | $(SWIFT) package clean 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swiftbrew ![](https://github.com/swiftbrew/Swiftbrew/workflows/Build/badge.svg) 2 | 3 | A package manager that installs prebuilt Swift command line tool packages, or _Homebrew for Swift packages_. 4 | 5 | ## Installation 6 | ### Homebrew 7 | 8 | ``` 9 | brew install swiftbrew/tap/swiftbrew 10 | ``` 11 | 12 | ### Mint 13 | 14 | ``` 15 | mint install swiftbrew/Swiftbrew 16 | ``` 17 | 18 | ### Swiftbrew 19 | 20 | ``` 21 | swift brew install swiftbrew/Swiftbrew 22 | ``` 23 | 24 | ## Usage 25 | 26 | ``` 27 | swift brew install 28 | ``` 29 | 30 | Package reference can be a shorthand for a GitHub repository 31 | (`Carthage/Carthage`) or a full git URL 32 | (`https://github.com/Carthage/Carthage.git`), optionally followed by a tagged 33 | version (`@x.y.z`). Swiftbrew currently only supports public repositories. 34 | 35 | ### Examples 36 | 37 | Install the latest version of `Carthage`: 38 | 39 | ``` 40 | swift brew install Carthage/Carthage 41 | ``` 42 | or 43 | 44 | ``` 45 | swift brew install https://github.com/Carthage/Carthage 46 | ``` 47 | 48 | Install `Carthage` version 0.33.0: 49 | 50 | ``` 51 | swift brew install Carthage/Carthage@0.33.0 52 | ``` 53 | 54 | ## Why create another package manager? 55 | 56 | [Homebrew](https://brew.sh) is a popular method of distributing command line 57 | tools on macOS. Some popular Swift command line tools are already distributed 58 | via Homebrew. But there are some limitations: 59 | 60 | - Distributing via Homebrew requires you to create a formula and then maintain 61 | that formula. 62 | - If your package is not popular enough to be accepted into Homebrew's core 63 | formulae, you would have to create and maintain your own tap. 64 | - As a package maintainer, a usual release process would be: build the 65 | executable, archive it into a tarball/zipball, upload it to GitHub releases, 66 | bump formula version. This is a cumbersome process. 67 | - It can be tricky to install a specific version of a tool with Homebrew. 68 | 69 | [Mint](https://github.com/yonaskolb/Mint) is a package manager that builds and 70 | installs Swift command line tool packages. Mint is more flexible than Homebrew 71 | as it allows installing a specific version of a package. The downside of Mint 72 | is that it requires you to build all packages from source. This can be very 73 | time-consuming as you start replacing most of your Ruby tools in your iOS 74 | project with Swift packages, since bumping a tool version would require 75 | rebuilding it from all your developers' machines. 76 | 77 | ### Introducing Swiftbrew 78 | 79 | **Swiftbrew** saves Swift packages maintainers and users' time by caching 80 | prebuilt Swift command line tool packages, while flexible enough to let users 81 | install multiple versions of a package. Swiftbrew builds and caches Swift 82 | packages on CDN servers so that they are fast to download from anywhere. 83 | Swiftbrew bottles (prebuilt packages) are hosted on 84 | [Bintray](http://bintray.com), the same service that hosts Homebrew bottles. If 85 | any package is not available as a bottle, it will be built by Swiftbrew build 86 | workers and cached after the first installation request, so that it will 87 | available for everyone later on. Here is what an installation output looks 88 | like: 89 | 90 | ``` 91 | $ swift brew install Carthage/Carthage 92 | 93 | ==> Finding latest version of Carthage 94 | Resolved latest version of Carthage to 0.33.0 95 | ==> Installing Carthage 0.33.0 96 | ==> Downloading https://dl.bintray.com/swiftbrew/bottles/github.com_Carthage_Carthage-0.33.0.mojave.tar.xz 97 | Bottle not yet available. Sent a build request to build workers. 98 | ==> Waiting for bottle to be available... 99 | ==> Pouring github.com_Carthage_Carthage-0.33.0.mojave.tar.xz 100 | 🍺 /usr/local/lib/swiftbrew/cellar/github.com_Carthage_Carthage/build/0.33.0 101 | ``` 102 | 103 | ## FAQ 104 | 105 | *What kind of packages can Swiftbrew install?* 106 | 107 | > Swiftbrew can install any public Swift command line tool package. If your 108 | > package can be built with `swift build` command, then it can be installed via 109 | > Swiftbrew. 110 | 111 | *Can I add my own package?* 112 | 113 | > Yes, if your package's Git URL is public. Just install your package with 114 | > Swiftbrew, a built request will be sent to Swiftbrew's build workers, then 115 | > the bottle will be available after a few minutes. 116 | 117 | *What platforms does Swiftbrew support?* 118 | 119 | > We only have build workers that run macOS Mojave in the meantime. Other macOS 120 | > versions and Linux may be added in the future upon community requests. 121 | 122 | ## License 123 | 124 | MIT 125 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Args", 6 | "repositoryURL": "https://github.com/getGuaka/Args.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "f58452117e865bf1b7c6fc29a4368e207c1f5c24", 10 | "version": "0.1.1" 11 | } 12 | }, 13 | { 14 | "package": "Colorizer", 15 | "repositoryURL": "https://github.com/getGuaka/Colorizer.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "2ccc99bf1715e73c4139e8d40b6e6b30be975586", 19 | "version": "0.2.1" 20 | } 21 | }, 22 | { 23 | "package": "Guaka", 24 | "repositoryURL": "https://github.com/nsomar/Guaka.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "6fb29b2378166a30d72120980e1c099c664598de", 28 | "version": "0.4.1" 29 | } 30 | }, 31 | { 32 | "package": "Just", 33 | "repositoryURL": "https://github.com/JustHTTP/Just.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "1824bf84cf52d11d69ae20cfb89f0ce5bffa5650", 37 | "version": "0.8.0" 38 | } 39 | }, 40 | { 41 | "package": "Mint", 42 | "repositoryURL": "https://github.com/yonaskolb/Mint.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "6f6f5871e254462b4b380197756df18aa6f2b5c7", 46 | "version": "0.12.0" 47 | } 48 | }, 49 | { 50 | "package": "Nimble", 51 | "repositoryURL": "https://github.com/Quick/Nimble.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "e9d769113660769a4d9dd3afb855562c0b7ae7b0", 55 | "version": "7.3.4" 56 | } 57 | }, 58 | { 59 | "package": "PathKit", 60 | "repositoryURL": "https://github.com/kylef/PathKit.git", 61 | "state": { 62 | "branch": null, 63 | "revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0", 64 | "version": "0.9.2" 65 | } 66 | }, 67 | { 68 | "package": "Prompt", 69 | "repositoryURL": "https://github.com/getGuaka/Prompt.git", 70 | "state": { 71 | "branch": null, 72 | "revision": "265f929fda31c59d7dc5f1c674fd435e41dd9881", 73 | "version": "0.1.1" 74 | } 75 | }, 76 | { 77 | "package": "Quick", 78 | "repositoryURL": "https://github.com/Quick/Quick.git", 79 | "state": { 80 | "branch": null, 81 | "revision": "f2b5a06440ea87eba1a167cab37bf6496646c52e", 82 | "version": "1.3.4" 83 | } 84 | }, 85 | { 86 | "package": "Rainbow", 87 | "repositoryURL": "https://github.com/onevcat/Rainbow.git", 88 | "state": { 89 | "branch": null, 90 | "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", 91 | "version": "3.1.5" 92 | } 93 | }, 94 | { 95 | "package": "Run", 96 | "repositoryURL": "https://github.com/getGuaka/Run.git", 97 | "state": { 98 | "branch": null, 99 | "revision": "5a2866c0ae4266e0406d85f16adf65a30a626d7f", 100 | "version": "0.1.1" 101 | } 102 | }, 103 | { 104 | "package": "Spectre", 105 | "repositoryURL": "https://github.com/kylef/Spectre.git", 106 | "state": { 107 | "branch": null, 108 | "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", 109 | "version": "0.9.0" 110 | } 111 | }, 112 | { 113 | "package": "StringScanner", 114 | "repositoryURL": "https://github.com/getGuaka/StringScanner.git", 115 | "state": { 116 | "branch": null, 117 | "revision": "de1685ad202cb586d626ed52d6de904dd34189f3", 118 | "version": "0.4.1" 119 | } 120 | }, 121 | { 122 | "package": "SwiftPM", 123 | "repositoryURL": "https://github.com/apple/swift-package-manager.git", 124 | "state": { 125 | "branch": null, 126 | "revision": "63a01220e93271dc3bf204b9e13dd1aeb2beefee", 127 | "version": "0.2.0" 128 | } 129 | }, 130 | { 131 | "package": "SwiftCLI", 132 | "repositoryURL": "https://github.com/jakeheis/SwiftCLI.git", 133 | "state": { 134 | "branch": null, 135 | "revision": "5318c37d3cacc8780f50b87a8840a6774320ebdf", 136 | "version": "5.2.2" 137 | } 138 | } 139 | ] 140 | }, 141 | "version": 1 142 | } 143 | -------------------------------------------------------------------------------- /Sources/SwiftbrewCore/SwiftbrewCore.swift: -------------------------------------------------------------------------------- 1 | import Colorizer 2 | import Foundation 3 | import Just 4 | import MintKit 5 | import Run 6 | import Utility 7 | 8 | #if DEBUG 9 | private let baseURL = "http://localhost:8081/bottles" 10 | private let buildTriggerURL = "http://localhost:8082/bottles" 11 | #else 12 | private let baseURL = "https://dl.bintray.com/swiftbrew/bottles" 13 | private let buildTriggerURL = "https://swiftbrew-server.herokuapp.com/bottles" 14 | #endif 15 | 16 | private let prefix = "/usr/local" 17 | private let binDirectory = "\(prefix)/bin" 18 | 19 | private let swiftbrewHomePath = "\(prefix)/lib/swiftbrew" 20 | private let cellarPath = "\(swiftbrewHomePath)/cellar" 21 | private let cachesPath = "\(swiftbrewHomePath)/caches" 22 | 23 | private let maxTryCount = 20 24 | private let waitingInterval: UInt32 = 30 25 | private let delayInterval: UInt32 = 3 26 | 27 | public func install(package: String) throws { 28 | let packageRef = PackageReference(package: package) 29 | 30 | if packageRef.version.isEmpty { 31 | try resolvePackageVersion(packageRef) 32 | } 33 | 34 | let repoPath = packageRef.repoPath 35 | let packageVersion = packageRef.version 36 | 37 | printProcessingInfo("Installing \(packageRef.namedVersion)") 38 | 39 | // We only have macOS Mojave for now, but that might change soon 40 | let cachedBottleFilename = repoPath + "-\(packageVersion).\(currentPlatformName()).tar.xz" 41 | let bottleURL = URL(string: baseURL)!.appendingPathComponent(cachedBottleFilename) 42 | 43 | printProcessingInfo("Downloading \(bottleURL)") 44 | 45 | let cachedBottlePath = "\(cachesPath)/\(cachedBottleFilename)" 46 | 47 | var bottleData: Data? = nil 48 | var tryCount = 0 49 | 50 | repeat { 51 | let result = Just.get(bottleURL) 52 | if result.ok, let data = result.content { 53 | bottleData = data 54 | break 55 | } 56 | 57 | if tryCount == 0 { 58 | printInfo("Bottle not yet available. Sent a build request to build workers.") 59 | let res = Just.post( 60 | buildTriggerURL, 61 | data: [ 62 | "name": packageRef.repoPath, 63 | "gitURL": packageRef.gitPath, 64 | "version": packageRef.version 65 | ] 66 | ) 67 | if let error = res.error { 68 | printError(error.localizedDescription) 69 | } 70 | } 71 | 72 | printProcessingInfo("Waiting for bottle to be available...") 73 | tryCount += 1 74 | 75 | if tryCount == maxTryCount { 76 | printInfo("Bottle still not available after \(tryCount * Int(waitingInterval) / 60) minutes wait.") 77 | printInfo("Check build log: https://app.bitrise.io/build/b48f2d2fe0b698c1") 78 | printInfo("File an issue: https://github.com/swiftbrew/Swiftbrew/issues/new") 79 | exit(1) 80 | } 81 | 82 | sleep(waitingInterval) 83 | } while tryCount < maxTryCount 84 | 85 | let makeCachesDir = run("mkdir -p \(cachesPath)") 86 | guard makeCachesDir.exitStatus == 0 else { 87 | printError(makeCachesDir.stderr) 88 | exit(1) 89 | } 90 | 91 | guard FileManager.default.createFile( 92 | atPath: cachedBottlePath, 93 | contents: bottleData, 94 | attributes: nil) 95 | else { 96 | printError("Failed to write to path \(cachedBottlePath)") 97 | exit(1) 98 | } 99 | 100 | printProcessingInfo("Pouring \(cachedBottleFilename)") 101 | 102 | let makeCellarDir = run("mkdir -p \(cellarPath)") 103 | guard makeCellarDir.exitStatus == 0 else { 104 | printError(makeCellarDir.stderr) 105 | exit(1) 106 | } 107 | 108 | let tar = run("tar xf \(cachesPath)/\(cachedBottleFilename) -C \(cellarPath)") 109 | guard tar.exitStatus == 0 else { 110 | printError(tar.stderr) 111 | exit(1) 112 | } 113 | 114 | let installPath = "\(cellarPath)/\(repoPath)/build/\(packageVersion)" 115 | 116 | try linkExecutables(installPath: installPath) 117 | 118 | printInfo("🍺 \(installPath)") 119 | } 120 | 121 | public func uninstall(name: String) throws { 122 | var installedPackages: [String] 123 | 124 | do { 125 | installedPackages = try listPackages() 126 | } catch { 127 | printError(error.localizedDescription) 128 | exit(1) 129 | } 130 | 131 | let packages = installedPackages.filter { 132 | $0.components(separatedBy: "/").last == name 133 | } 134 | 135 | switch packages.count { 136 | case 0: 137 | printError("No such package \(name)") 138 | exit(1) 139 | case 1: 140 | try deletePackage(packages.first!) 141 | printInfo("🗑 \(name) was uninstalled") 142 | default: 143 | for package in packages { 144 | try deletePackage(package) 145 | } 146 | 147 | printInfo("🗑 \(packages.count) packages match the name \(name) was uninstalled") 148 | } 149 | } 150 | 151 | // MARK: - Private 152 | 153 | private func linkExecutables(installPath: String) throws { 154 | let ls = run("/bin/ls \(installPath)") 155 | guard ls.exitStatus == 0 else { 156 | printError(ls.stderr) 157 | exit(1) 158 | } 159 | 160 | let executables = ls 161 | .stdout 162 | .split(separator: "\n") 163 | .map(String.init) 164 | 165 | for executable in executables { 166 | let executablePath = "\(installPath)/\(executable)" 167 | let symlinkPath = "\(binDirectory)/\(executable)" 168 | let ln = run("/bin/ln -sf \(executablePath) \(symlinkPath)") 169 | 170 | guard ln.exitStatus == 0 else { 171 | printError(ln.stderr) 172 | exit(1) 173 | } 174 | } 175 | } 176 | 177 | private func resolvePackageVersion(_ package: PackageReference) throws { 178 | // We don't have a specific version, let's get the latest tag 179 | printProcessingInfo("Finding latest version of \(package.name)") 180 | 181 | let tagOutput = run("git ls-remote --tags --refs \(package.gitPath)") 182 | let tagReferences = tagOutput.stdout 183 | 184 | if tagReferences.isEmpty { 185 | let headOutput = run("git ls-remote --heads \(package.gitPath)") 186 | let headReferences = headOutput.stdout 187 | package.version = headReferences.split(separator: "\t").map(String.init).first! 188 | } else { 189 | let tags = tagReferences.split(separator: "\n").map { String($0.split(separator: "\t").last!.split(separator: "/").last!) } 190 | let versions = Git.convertTagsToVersionMap(tags) 191 | if let latestVersion = versions.keys.sorted().last, let tag = versions[latestVersion] { 192 | package.version = tag 193 | } else { 194 | package.version = "master" 195 | } 196 | } 197 | 198 | printInfo("Resolved latest version of \(package.name) to \(package.version)") 199 | } 200 | 201 | // Return the list of installed packages in short-hand form. 202 | // Eg: ["thii/xcbeautify"] 203 | private func listPackages() throws -> [String] { 204 | guard let url = URL(string: cellarPath) else { 205 | fatalError() 206 | } 207 | 208 | let packageURLs = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) 209 | 210 | return packageURLs.compactMap { (url) in 211 | let components = url.path.components(separatedBy: "_") 212 | guard components.count == 3 else { 213 | return nil 214 | } 215 | 216 | return components[1] + "/" + components[2] 217 | } 218 | } 219 | 220 | private func deletePackage(_ package: String) throws { 221 | let packageRef = PackageReference(package: package) 222 | let packagePath = "\(cellarPath)/\(packageRef.repoPath)" 223 | 224 | printProcessingInfo("Uninstalling \(packageRef.namedVersion)") 225 | sleep(delayInterval) 226 | 227 | // Remove package 228 | // Note that all versions will be removed altogether 229 | let rm = run("/bin/rm -rf \(packagePath)") 230 | guard rm.exitStatus == 0 else { 231 | printError(rm.stderr) 232 | exit(1) 233 | } 234 | 235 | // Remove symbolic link silently 236 | let symlinkPath = "\(binDirectory)/\(packageRef.name)" 237 | _ = run("/bin/rm \(symlinkPath)") 238 | } 239 | --------------------------------------------------------------------------------