Swift from the command line

Published: Sep 10, 2025

Last updated: Sep 10, 2025

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() } }

In the above code, we use `CFRunLoopRun`, `CFRunLoopStop` and `CFRunLoopGetMain` functions for writing backwards-compatible Alamofire code. If you want to use async/await, you can update your `Package.swift` platforms to target higher versions of MacOS (assuming you are on MacOS).

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

Personal image

Dennis O'Keeffe

Byron Bay, Australia

Share this post

Recommended articles

Dennis O'Keeffe

2020-present Dennis O'Keeffe.

All Rights Reserved.