Swift from the command line

An introduction to using Swift without Xcode

On this page

The following blog post is simply a recount of me tinkering with Swift without using Xcode.

It explores initializing an executable, working through some simple Swift syntax and managing packages with SwiftPM (Swift Package Manager).

Prerequisites

  • Swift (this post uses Swift 6.1).

Initializing a Swift executable package

You can create a new Swift executable package with the following (from the command line):

$ mkdir hello-swift
$ cd hello-swift
$ swift package init --type executable

You can run swift package init --help to get more information around arguments like --type. For example:

swift package init --help
OVERVIEW: Initialize a new package

USAGE: swift package init [--type <type>] [--enable-xctest] [--disable-xctest] [--enable-swift-testing] [--disable-swift-testing] [--name <name>]

OPTIONS:
  --type <type>           Package type: (default: library)
        library           - A package with a library.
        executable        - A package with an executable.
        tool              - A package with an executable that uses
                            Swift Argument Parser. Use this template if you
                            plan to have a rich set of command-line arguments.
        build-tool-plugin - A package that vends a build tool plugin.
        command-plugin    - A package that vends a command plugin.
        macro             - A package that vends a macro.
        empty             - An empty package with a Package.swift manifest.
  --enable-xctest/--disable-xctest
                          Enable support for XCTest
  --enable-swift-testing/--disable-swift-testing
                          Enable support for Swift Testing
  --name <name>           Provide custom package name
  --version               Show the version.
  -h, -help, --help       Show help information.

With this new package initialized as an executable, you can navigate to Sources/main.swift to see the generated entry file:

/ The Swift Programming Language
// https://docs.swift.org/swift-book

print("Hello, world!")

Running our executable

You can make use of swift run to run our executable:

$ swift run
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product 'hello-swift' complete! (0.19s)
Hello, world!

We can re-use this to run after any file changes:

$ swift run
Building for debugging...
[7/7] Applying hello-swift
Build of product 'hello-swift' complete! (1.01s)
Hello again, world!

If you have a program installed such as watchexec, you can set it to watch swift extension files with watchexec --exts swift -- swift run :

$ watchexec --exts swift -- swift run
[Running: swift run]
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product 'hello-swift' complete! (0.20s)
Hello again, world!
[Command was successful]
[Running: swift run]
[1/1] Planning build
Building for debugging...
[7/7] Applying hello-swift
Build of product 'hello-swift' complete! (0.73s)
Hello again and again, world!
[Command was successful]

Building files

You can run swift build from the project root to build out the executable.

$ swift build
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build complete! (0.23s)

By default, swift build will build with debug configuration.

Outputs are placed in .build:

$ tree .build -L 3
.build
├── arm64-apple-macosx
│   └── debug
│       ├── description.json
│       ├── hello_swift.build
│       ├── hello-swift
│       ├── hello-swift-entitlement.plist
│       ├── hello-swift.dSYM
│       ├── hello-swift.product
│       ├── index
│       ├── ModuleCache
│       ├── Modules
│       ├── plugin-tools-description.json
│       └── swift-version--58304C5D6DBC2206.txt
├── artifacts
├── build.db
├── checkouts
├── debug -> arm64-apple-macosx/debug
├── debug.yaml
├── plugin-tools.yaml
├── repositories
└── workspace-state.json

For an optimized build, pass --configuration release:

$ swift build --configuration release
Building for production...
[5/5] Linking hello-swift
Build complete! (1.21s)
$ tree .build -L 3
.build
├── arm64-apple-macosx
│   ├── debug
│   │   ├── description.json
│   │   ├── hello_swift.build
│   │   ├── hello-swift
│   │   ├── hello-swift-entitlement.plist
│   │   ├── hello-swift.dSYM
│   │   ├── hello-swift.product
│   │   ├── index
│   │   ├── ModuleCache
│   │   ├── Modules
│   │   ├── plugin-tools-description.json
│   │   └── swift-version--58304C5D6DBC2206.txt
│   └── release
│       ├── description.json
│       ├── hello_swift.build
│       ├── hello-swift
│       ├── hello-swift.dSYM
│       ├── hello-swift.product
│       ├── ModuleCache
│       ├── Modules
│       ├── plugin-tools-description.json
│       └── swift-version--58304C5D6DBC2206.txt
├── artifacts
├── build.db
├── checkouts
├── debug -> arm64-apple-macosx/debug
├── debug.yaml
├── plugin-tools.yaml
├── release -> arm64-apple-macosx/release
├── release.yaml
├── repositories
└── workspace-state.json

20 directories, 14 files

Once compiled, you'll have an optimized binary you can run directly (if your platform is the correct target):

$ ./.build/release/hello-swift
Hello again and again, world!

Other useful flags include --triple to cross-compile for another platform.

Makefile

You can make use of a Makefile for common operations:

.PHONY: build run clean release test

build:
	swift build

release:
	swift build -c release

run:
	swift run

test:
	swift test

clean:
	swift package clean

In use:

$ make clean
swift package clean

$ make build
swift build
Building for debugging...
[8/8] Applying hello-swift
Build complete! (1.06s)

$ make run
swift run
[1/1] Planning build
Building for debugging...
[1/1] Write swift-version--58304C5D6DBC2206.txt
Build of product 'hello-swift' complete! (0.21s)
Hello again and again, world!

Managing packages

You can manage packages in Package.swift or via the command line.

The default file that was created:

// Package.swift
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "hello-swift",
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "hello-swift"),
    ]
)

From the command line, you can install a package using swift package add-dependency <URL>:

$ swift package add-dependency https://github.com/Alamofire/Alamofire --from 5.10.2
Updating package manifest at Package.swift... done.

In the above case, the package is installed via the GitHub repository for the popular networking package Alamofire.

You must also provide a version.

$ swift package add-dependency https://github.com/Alamofire/Alamofire
error: must specify one of --exact, --branch, --revision, --from, or --up-to-next-minor-from

Once installed, your Package.swift file will be updated:

// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "hello-swift",
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.10.2"),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "hello-swift"),
    ]
)

In order to use it, you must update the relevant targets:

// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "hello-swift",
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.10.2"),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "hello-swift",
            dependencies: [
                "Alamofire"   // 👈 add this line
            ]),
    ]
)

We can see this in action by updating our main.swift file:

// Sources/main.swift
// The Swift Programming Language
// https://docs.swift.org/swift-book

import Alamofire
import Foundation

@main
struct Hello {
    static func main() {
        AF.request("https://jsonplaceholder.typicode.com/todos/1")
            .responseString { response in
                print(response.value ?? "no data")
                CFRunLoopStop(CFRunLoopGetMain()) // stop loop when done
            }

        // keep the program alive until CFRunLoopStop is called
        CFRunLoopRun()
    }
}

Let's see it in action:

$ swift run
Fetching https://github.com/Alamofire/Alamofire
Fetched https://github.com/Alamofire/Alamofire from cache (22.64s)
Computing version for https://github.com/Alamofire/Alamofire
Computed https://github.com/Alamofire/Alamofire at 5.10.2 (23.82s)
Creating working copy for https://github.com/Alamofire/Alamofire
Working copy of https://github.com/Alamofire/Alamofire resolved at 5.10.2
[1/1] Planning build
Building for debugging...
[52/52] Applying hello-swift
Build of product 'hello-swift' complete! (6.15s)
{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

Perfect, we got our response from an installed package.

Conclusion

This post was recounting some baby steps with initializing Swift packages. I will likely be writing a few tools for MacOS with this approach for learning more about the language while diving into iOS.

Photo credit: johnnyb803