alias swiftxcode="swift package generate-xcodeproj --xcconfig-overrides Package.xcconfig"
alias swiftbuildMacOS='swift build -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.13"'
alias swifttest='swift test -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.13"
highway
allows you to quickly automate the build, test and release cycle of your iOS or macOS app. Since highway
builds on technologies you already know (Swift & the Swift Package Manager, Foundation, ...) getting started is super easy.
Command | Description |
---|---|
highway init |
Initializes a new highway project in the current directory. |
highway help |
Displays available commands and options. |
highway generate |
Generates an Xcode project. |
highway bootstrap |
Bootstraps the highway home directory. |
highway clean |
Delete build artifacts of your highway project. |
highway self_update |
Updates highway & the supporting frameworks |
highway --version |
Print version information and exit. |
Good to know
- Unknown commands/arguments are passed to your highway project. For example:
highway xyz
will execute your highway calledxyz
. - Executing
highway
without any arguments will print all available commands (including commands implemented by your highway project). - To see whats going on under the hood use
--verbose
. For example:highway --verbose xyz
.
Simply paste the following command into a terminal of your choice.
pushd $(mktemp -d -t highway) && \
git clone -b master https://github.com/ChristianKienle/highway.git highway && \
./highway/scripts/bootstrap.sh --interactive && popd
This will checkout highway and run the bootstrap script. The bootstrap script is building highway using Swift Package Manager. The built highway command line tool will be placed (alongside other stuff) in ~/.highway
. Make sure to add ~/.highway/bin
to your $PATH
after the bootstrap process is finished.
After installing highway make sure to add ~/.highway/bin
to your $PATH
. Here is one way to do it:
$ echo "PATH=$PATH:${HOME}/.highway/bin" >> ${HOME}/.profile
Then open a new terminal window, so that the new PATH
takes effect. Check your installation:
$ highway --version
$ highway help
With highway you can do just about anything. Highway is not specifically made for a specific task. You can think of it as a command line tool (+ support frameworks) that make is easy to manage, build and execute Swift code from the command line. One use case that comes immediately to mind is to use highway to build, test, deploy and release iOS/macOS apps.
Open Terminal and create an empty directory:
$ mkdir my_new_project
$ cd my_new_project
Now you can create a plain highway project.:
$ highway init
This creates a directory directory named _highway
. You can have a look at the contents of the directory if you want. It is just a Swift Package Manager compatible project with a single Swift file, main.swift
. In the directory containing the _highway
directory execute:
$ highway
Yes: Without any arguments. This builds and runs your _highway
. By default, this displays a list of available commands. The cool thing is that you can add new commands by simply editing the _highway/main.swift
file.
The highway
command line tool can be executed with arguments - just like most command line tools. Custom highways make it possible to invoke highway
with custom commands/arguments. You can think of a highway as code that maps arguments/commands to custom logic, written in Swift. Each new highway project (created using highway init
) comes with a few custom highways by default. You can see the predefined custom highways in _highway/main.swift
. The default custom highways look something like this:
import HighwayCore
import XCBuild
import HighwayProject
import Deliver
import Foundation
enum Way: String, HighwayType {
case test, build, run
var usage: String {
switch self {
case .build: return "Builds the project"
case .test: return "Executes tests"
case .run: return "Runs the project"
}
}
}
class App: Highway<Way> {
override func setupHighways() {
self[.build] ==> build
self[.test] ==> test
self[.run] ==> run
}
// MARK: - Highways
func build() {
}
func test() throws {
var options = TestOptions()
options.project = "<insert path to *.xcproject here>"
options.scheme = "<insert name of scheme here>"
options.destination = Destination.simulator(.iOS, name: "iPhone 7", os: .latest, id: nil)
try xcbuild.buildAndTest(using: options)
}
func run() {
}
}
App(Way.self).go()
The Way
-enum implements the HighwayType
-protocol. By default the rawValue
of the Way
-enum will be the expected argument. In the example above the highways build
, test
and run
can be invoked like this:
Executing the build
-highway:
$ highway build
Executing the test
-highway:
$ highway test
Executing the run
-highway:
$ highway run
You get the idea. Creating a custom highway is done in two steps:
1. Add a case to the Highway
-implementation and implement -usage
:
//...
enum Way: String, HighwayType {
case test
case build
case run
case myHighway // β¬
οΈ HERE
var usage: String {
switch self {
case .build: return "Builds the project"
case .test: return "Executes tests"
case .run: return "Runs the project"
case .myHighway: return "hello" // β¬
οΈ HERE
}
}
}
//...
2. Register your highway:
Still in _highway/main.swift
: Scroll down and register your highway in -setupHighways
like this:
//...
class App: Highway<Way> {
override func setupHighways() {
self[.build] ==> build
self[.test] ==> test
self[.run] ==> run
self[.myHighway] ==> myHighway // β¬
οΈ HERE
}
func myHighway() throws { // β¬
οΈ HERE
print("hello world")
}
//...
Now you can execute your highway like this:
$ highway myHighway
If you omit the command/highway then highway will list all available commands/highways.
A highway can depend on other highways. You specify dependencies between highways in your implementation of -setupHighways
:
//...
override func setupHighways() {
// imagine 'build' actually builds your app/project.
self[.build] ==> build
// imagine 'test' actually runs your tests.
self[.test].depends(on: .build) ==> test
// imagine 'run 'actually runs your app/project.
self[.run].depends(on: .test) ==> run
}
//...
In the example above there are three highways, two of which depend on other highways.
test
depends onbuild
: This means that executinghighway test
first executes thebuild
highway. Which makes sense because you want to execute the tests only if your project is building.run
depends ontest
: This means that executinghighway run
first executes thetest
highway (which first executes thebuild
highway). This way it is ensured thathighway run
always runs the latest artifact.
A highway can depend on a single highway (see above) or on multiple highways. Multiple dependencies are specified by using the exact same method (-depend(on:)
). For example:
//...
override func setupHighways() {
// imagine 'build' actually builds your app/project.
self[.build] ==> build
// imagine 'test' actually runs your tests.
self[.test] ==> test
// imagine 'run 'actually runs your app/project.
self[.run].depends(on: .build, .test) ==> run
}
//...
The run
-highway above directly depends on build
and test
. This means that highway run
will first execute build
and then test
.
A highway usually performs a task like building, testing, deploying (or something less impactful). It is not uncommon that a highway produces some kind of (intermediate) result. One example immediately comes to mind:
Let's assume that you have two highways:
build
: Builds your project - for example by usingxcodebuild
orswift
.release
: Releases your project by uploading the build artifact to a server.
Your release
-highway naturally depends on build
: build
must be executed before release
and release
needs the results from the build process. You can do that by combining dependencies and highways with results:
import HighwayCore
import XCBuild
import HighwayProject
import Deliver
import Foundation
enum Way: String, HighwayType {
case build, release
}
class App: Highway<Way> {
override func setupHighways() {
self[.build] ==> build
self[.release].depends(on: .build) ==> release
}
func build() -> String {
return "./.build/release/my_app"
}
func release() throws {
let path: String = try result(for: .build)
print(path)
// Now upload the file at path to a server.
}
}
App(Way.self).go()
There are a few special highways. Registering for a special highway is straight forward:
//...
override func setupHighways() {
self[.build] ==> build
self[.release].depends(on: .build) ==> release
// π π SPECIAL HIGHWAYS π π
onError = { print($0) } // 1.
onEmptyCommand = { print("$ ./_highway") } // 2.
onUnrecognizedCommand = { print("args: \($0)")} // 3.
}
//...
The code above registers for three special highways:
onError
: If a custom highway throws anSwift.Error
this highway is executed. The error is passed as an argument.onEmptyCommand
: If the_highway
binary is executed without any command this highway is executed. Don't confusehighway
with_highway
.highway
is the command line tool you interact with all the time._highway
is the executable produced by compiling_highway/main.swift
. Usually you do never interact with_highway
directly. You only use_highway
indirectly. It is only listed here to be complete.onUnrecognizedCommand
: Ifhighway
is executed with a command that is neither handled by itself nor by your_highway
project the default behavior is thathighway
prints a list of all known commands β unless you have registered theonUnrecognizedCommand
highway. In that casehighway
no longer prints any helpful information when it encounters an unknown command.
highway comes with a few frameworks that help you get stuff done in your highway project. You are not limited to only those frameworks/features. You can use almost any Objective-C/Swift framework available. Just add additional frameworks you wanna use in your _highway/main.swift
-file to _highway/Package.swift
. However in some circumstances, the build in features/frameworks will be enough. Here is what you can use out of the box:
Example:
let swift = SwiftBuildSystem()
try swift.test() // Test
// Build and get an object describing the result.
let artifact = try swift.build()
print("π \(artifact.binPath)")
print("π \(artifact.buildOutput)")
SwiftBuildSystem
is a wrapper around the swift
command line tool. By default, test()
simply executes the tests of the Swift project in _highway/..
. build()
is much like test()
with the difference that it returns a SwiftBuildSystem.Artifact
that has properties like buildOutput
(everything swift build
would have printed to the screen) and binPath
, the path to the directory that contains the built executable.
highway has a few classes that deal with Xcode (a wrapper for xcodebuild and xcpretty). They can be found in the XCBuild
framework.
Those classes can be used to easily talk to the Xcode build system. However they are very volatile and subject to change.
Each Highway
automatically has an instance of XCBuild
. XCBuild
is able to:
- build your Xcode project,
- run your tests,
- create archives,
- code sign and export those archives and
- finally upload them to iTunes Connect
For example the following code (that can be used in setupHighways
as a highway) builds and tests the project myapp
.
func build() throws -> TestReport {
var options = TestOptions()
options.project = cwd.appending("myapp.xcodeproj").path
options.destination = Destination.simulator(.iOS,
name: "iPhone 7",
os: .latest,
id: nil)
options.scheme = "myapp"
return try xcbuild.buildAndTest(using: options)
}
A simple way to store secrets and use the from within your custom highways is to use Keychain
. You could store your old fashioned and highly insecure FTP-passwords there for example. Highway comes with a simple class that allows you to access your keychain:
let keychain = Keychain()
let query = Keychain.PasswordQuery(account: "$username", service: "My FTP Password")
let password = try keychain.password(matching: query)
let git = self.git // each highway has a git-instance already!
let cwd = self.cwd // each highway knows it's current working directory
// Executes: git add .
try git.addEverything(at: cwd)
// Executes: git commit -m "$message"
try git.commit(at: cwd, message: "$message")
// Executes: git push origin master
try git.pushToMaster(at: cwd)
// Executes: git-autotag
let nextVersion = try GitAutotag().autotag(at: cwd, dryRun: false)
// Executes: git push --tags
try git.pushTagsToMaster(at: cwd)
A super minimalistic wrapper around fastlane:
try Fastlane().gym("arguments", "passed", "to", "fastlane gym")
try Fastlane().scan("arguments", "passed", "to", "fastlane scan")
Update the dependencies of your highway project:
$ highway clean
$ highway
$ highway generate # optional
Update highway itself:
$ highway self_update
$ highway --version # verify