Skip to content

Commit 23bb23a

Browse files
authoredNov 17, 2024··
migrate from make to sake (#5)
1 parent 56a7685 commit 23bb23a

11 files changed

+517
-104
lines changed
 

‎.github/workflows/tests.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ jobs:
1212
- uses: swift-actions/setup-swift@v2
1313
with:
1414
swift-version: "5.10"
15+
- name: Prepare test build
16+
run: swift build
1517
- name: Run tests
16-
run: make test
18+
run: ./Tests/integration_tests.sh .build/debug/progressline

‎.sake.yml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
case_converting_strategy: toSnakeCase

‎Makefile

-102
This file was deleted.

‎SakeApp/.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.DS_Store
2+
/.build
3+
/.index-build
4+
/Packages
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/configuration/registries.json
8+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9+
.netrc

‎SakeApp/BrewCommands.swift

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Sake
2+
import SwiftShell
3+
4+
@CommandGroup
5+
struct BrewCommands {
6+
static var ensureGhInstalled: Command {
7+
Command(
8+
description: "Ensure gh is installed",
9+
skipIf: { _ in
10+
run("which", "gh").succeeded
11+
},
12+
run: { _ in
13+
try runAndPrint("brew", "install", "gh")
14+
}
15+
)
16+
}
17+
18+
static var ensureGitCliffInstalled: Command {
19+
Command(
20+
description: "Ensure git-cliff is installed",
21+
skipIf: { _ in
22+
run("which", "git-cliff").succeeded
23+
},
24+
run: { _ in
25+
try runAndPrint("brew", "install", "git-cliff")
26+
}
27+
)
28+
}
29+
}

‎SakeApp/Package.resolved

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
"originHash" : "564ae29a93959e0a64ff9ad1a401e5db179007f3ecef20571f852606328631f6",
3+
"pins" : [
4+
{
5+
"identity" : "sake",
6+
"kind" : "remoteSourceControl",
7+
"location" : "https://github.com/kattouf/Sake",
8+
"state" : {
9+
"revision" : "f2c91c8ecb4f67f0c565b081deb7d180761a21d7",
10+
"version" : "0.2.2"
11+
}
12+
},
13+
{
14+
"identity" : "swift-argument-parser",
15+
"kind" : "remoteSourceControl",
16+
"location" : "https://github.com/apple/swift-argument-parser.git",
17+
"state" : {
18+
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
19+
"version" : "1.5.0"
20+
}
21+
},
22+
{
23+
"identity" : "swift-syntax",
24+
"kind" : "remoteSourceControl",
25+
"location" : "https://github.com/apple/swift-syntax",
26+
"state" : {
27+
"revision" : "64889f0c732f210a935a0ad7cda38f77f876262d",
28+
"version" : "509.1.1"
29+
}
30+
},
31+
{
32+
"identity" : "swiftshell",
33+
"kind" : "remoteSourceControl",
34+
"location" : "https://github.com/kareman/SwiftShell",
35+
"state" : {
36+
"revision" : "99680b2efc7c7dbcace1da0b3979d266f02e213c",
37+
"version" : "5.1.0"
38+
}
39+
},
40+
{
41+
"identity" : "yams",
42+
"kind" : "remoteSourceControl",
43+
"location" : "https://github.com/jpsim/Yams.git",
44+
"state" : {
45+
"revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d",
46+
"version" : "5.1.3"
47+
}
48+
}
49+
],
50+
"version" : 3
51+
}

‎SakeApp/Package.swift

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// swift-tools-version: 5.10
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import CompilerPluginSupport
5+
import PackageDescription
6+
7+
let package = Package(
8+
name: "SakeApp",
9+
platforms: [.macOS(.v10_15)], // Required by SwiftSyntax for the macro feature in Sake
10+
products: [
11+
.executable(name: "SakeApp", targets: ["SakeApp"]),
12+
],
13+
dependencies: [
14+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
15+
.package(url: "https://github.com/kattouf/Sake", from: "0.1.0"),
16+
.package(url: "https://github.com/kareman/SwiftShell", from: "5.1.0"),
17+
],
18+
targets: [
19+
.executableTarget(
20+
name: "SakeApp",
21+
dependencies: [
22+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
23+
"Sake",
24+
"SwiftShell",
25+
],
26+
path: "."
27+
),
28+
]
29+
)

‎SakeApp/ReleaseCommands.swift

+345
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
import ArgumentParser
2+
import Foundation
3+
import CryptoKit
4+
import Sake
5+
import SwiftShell
6+
7+
@CommandGroup
8+
struct ReleaseCommands {
9+
private struct BuildTarget {
10+
enum Arch {
11+
case x86
12+
case arm
13+
}
14+
enum OS {
15+
case macos
16+
case linux
17+
}
18+
19+
let arch: Arch
20+
let os: OS
21+
22+
var triple: String {
23+
switch (arch, os) {
24+
case (.x86, .macos): "x86_64-apple-macosx"
25+
case (.arm, .macos): "arm64-apple-macosx"
26+
case (.x86, .linux): "x86_64-unknown-linux-gnu"
27+
case (.arm, .linux): "aarch64-unknown-linux-gnu"
28+
}
29+
}
30+
}
31+
32+
private enum Constants {
33+
static let buildArtifactsDirectory = ".build/artifacts"
34+
static let swiftVersion = "6.0"
35+
static let buildTargets: [BuildTarget] = [
36+
.init(arch: .arm, os: .macos),
37+
.init(arch: .x86, os: .macos),
38+
.init(arch: .x86, os: .linux),
39+
.init(arch: .arm, os: .linux),
40+
]
41+
static let executableName = "progressline"
42+
}
43+
44+
private struct ReleaseArguments: ParsableArguments {
45+
@Argument(help: "Version number")
46+
var version: String
47+
48+
func validate() throws {
49+
guard version.range(of: #"^\d+\.\d+\.\d+$"#, options: .regularExpression) != nil else {
50+
throw ValidationError("Invalid version number. Should be in the format 'x.y.z'")
51+
}
52+
}
53+
}
54+
55+
public static var brewRelease: Command {
56+
Command(
57+
description: "Brew to Homebrew",
58+
run: { context in
59+
let arguments = try ReleaseArguments.parse(context.arguments)
60+
try arguments.validate()
61+
62+
let version = arguments.version
63+
try runAndPrint("brew", "bump-formula-pr", "--version=\(version)", "progressline")
64+
}
65+
)
66+
}
67+
68+
public static var githubRelease: Command {
69+
Command(
70+
description: "Release to GitHub",
71+
dependencies: [
72+
bumpVersion,
73+
cleanReleaseArtifacts,
74+
buildReleaseArtifacts,
75+
calculateBuildArtifactsSha256,
76+
createAndPushTag,
77+
generateReleaseNotes,
78+
draftReleaseWithArtifacts,
79+
]
80+
)
81+
}
82+
83+
static var bumpVersion: Command {
84+
Command(
85+
description: "Bump version",
86+
skipIf: { context in
87+
let arguments = try ReleaseArguments.parse(context.arguments)
88+
try arguments.validate()
89+
90+
let version = arguments.version
91+
let versionFilePath = "Sources/Version.swift"
92+
let currentVersion = try String(contentsOfFile: versionFilePath)
93+
.split(separator: "\"")[1]
94+
if currentVersion == version {
95+
print("Version is already \(version). Skipping...")
96+
return true
97+
} else {
98+
return false
99+
}
100+
},
101+
run: { context in
102+
let arguments = try ReleaseArguments.parse(context.arguments)
103+
try arguments.validate()
104+
105+
let version = arguments.version
106+
let versionFilePath = "Sources/Version.swift"
107+
let versionFileContent = """
108+
// This file is autogenerated. Do not edit.
109+
let progressLineVersion = "\(version)"
110+
111+
"""
112+
try versionFileContent.write(toFile: versionFilePath, atomically: true, encoding: .utf8)
113+
114+
try runAndPrint("git", "add", versionFilePath)
115+
try runAndPrint("git", "commit", "-m", "chore(release): Bump version to \(version)")
116+
print("Version bumped to \(version)")
117+
}
118+
)
119+
}
120+
121+
static var cleanReleaseArtifacts: Command {
122+
Command(
123+
description: "Clean release artifacts",
124+
run: { _ in
125+
try? runAndPrint("rm", "-rf", Constants.buildArtifactsDirectory)
126+
}
127+
)
128+
}
129+
130+
static var buildReleaseArtifacts: Command {
131+
Command(
132+
description: "Build release artifacts",
133+
skipIf: { context in
134+
let arguments = try ReleaseArguments.parse(context.arguments)
135+
try arguments.validate()
136+
let version = arguments.version
137+
138+
let targetsWithExistingArtifacts = Constants.buildTargets.filter { target in
139+
let archivePath = executableArchivePath(target: target, version: version)
140+
return FileManager.default.fileExists(atPath: archivePath)
141+
}
142+
if targetsWithExistingArtifacts.count == Constants.buildTargets.count {
143+
print("Release artifacts already exist. Skipping...")
144+
return true
145+
} else {
146+
context.storage["existing-artifacts-triples"] = targetsWithExistingArtifacts.map(\.triple)
147+
return false
148+
}
149+
},
150+
run: { context in
151+
let arguments = try ReleaseArguments.parse(context.arguments)
152+
try arguments.validate()
153+
let version = arguments.version
154+
155+
try FileManager.default.createDirectory(
156+
atPath: Constants.buildArtifactsDirectory,
157+
withIntermediateDirectories: true,
158+
attributes: nil
159+
)
160+
let existingArtifactsTriples = context.storage["existing-artifacts-triples"] as? [String] ?? []
161+
for target in Constants.buildTargets {
162+
if existingArtifactsTriples.contains(target.triple) {
163+
print("Skipping \(target.triple) as artifacts already exist")
164+
continue
165+
}
166+
let (swiftBuild, swiftClean, strip, zip) = {
167+
let buildFlags = ["--disable-sandbox", "--configuration", "release", "--triple", target.triple]
168+
if target.os == .linux {
169+
let platform = target.arch == .arm ? "linux/arm64" : "linux/amd64"
170+
let dockerExec = "docker run --rm --volume \(context.projectRoot):/workdir --workdir /workdir --platform \(platform) swift:\(Constants.swiftVersion)"
171+
let buildFlags = (buildFlags + ["--static-swift-stdlib"]).joined(separator: " ")
172+
return (
173+
"\(dockerExec) swift build \(buildFlags)",
174+
"\(dockerExec) swift package clean",
175+
"\(dockerExec) strip -s",
176+
"zip -j"
177+
)
178+
} else {
179+
let buildFlags = buildFlags.joined(separator: " ")
180+
return (
181+
"swift build \(buildFlags)",
182+
"swift package clean",
183+
"strip -rSTx",
184+
"zip -j"
185+
)
186+
}
187+
}()
188+
189+
try runAndPrint(bash: swiftClean)
190+
try runAndPrint(bash: swiftBuild)
191+
192+
let binPath: String = run(bash: "\(swiftBuild) --show-bin-path").stdout
193+
if binPath.isEmpty {
194+
throw NSError(domain: "Fail to get bin path", code: -999)
195+
}
196+
let executablePath = binPath + "/\(Constants.executableName)"
197+
198+
try runAndPrint(bash: "\(strip) \(executablePath)")
199+
200+
let executableArchivePath = executableArchivePath(target: target, version: version)
201+
try runAndPrint(bash: "\(zip) \(executableArchivePath) \(executablePath.replacingOccurrences(of: "/workdir", with: context.projectRoot))")
202+
}
203+
204+
print("Release artifacts built successfully at '\(Constants.buildArtifactsDirectory)'")
205+
}
206+
)
207+
}
208+
209+
static var calculateBuildArtifactsSha256: Command {
210+
@Sendable
211+
func shasumFilePath(version: String) -> String {
212+
".build/artifacts/shasum-\(version)"
213+
}
214+
215+
return Command(
216+
description: "Calculate SHA-256 checksums for build artifacts",
217+
skipIf: { context in
218+
let arguments = try ReleaseArguments.parse(context.arguments)
219+
try arguments.validate()
220+
let version = arguments.version
221+
222+
let shasumFilePath = shasumFilePath(version: version)
223+
224+
return FileManager.default.fileExists(atPath: shasumFilePath)
225+
},
226+
run: { context in
227+
let arguments = try ReleaseArguments.parse(context.arguments)
228+
try arguments.validate()
229+
let version = arguments.version
230+
231+
var shasumResults = [String]()
232+
for target in Constants.buildTargets {
233+
let archivePath = executableArchivePath(target: target, version: version)
234+
let file = FileHandle(forReadingAtPath: archivePath)!
235+
let shasum = SHA256.hash(data: file.readDataToEndOfFile())
236+
let shasumString = shasum.compactMap { String(format: "%02x", $0) }.joined()
237+
shasumResults.append("\(shasumString) \(archivePath)")
238+
}
239+
FileManager.default.createFile(
240+
atPath: shasumFilePath(version: version),
241+
contents: shasumResults.joined(separator: "\n").data(using: .utf8)
242+
)
243+
}
244+
)
245+
}
246+
247+
static var createAndPushTag: Command {
248+
Command(
249+
description: "Create and push a tag",
250+
skipIf: { context in
251+
let arguments = try ReleaseArguments.parse(context.arguments)
252+
try arguments.validate()
253+
254+
let version = arguments.version
255+
256+
let grepResult = run(bash: "git tag | grep \(arguments.version)")
257+
if grepResult.succeeded {
258+
print("Tag \(version) already exists. Skipping...")
259+
return true
260+
} else {
261+
return false
262+
}
263+
},
264+
run: { context in
265+
let arguments = try ReleaseArguments.parse(context.arguments)
266+
try arguments.validate()
267+
268+
let version = arguments.version
269+
270+
print("Creating and pushing tag \(version)")
271+
try runAndPrint("git", "tag", version)
272+
try runAndPrint("git", "push", "origin", "tag", version)
273+
try runAndPrint("git", "push") // push local changes like version bump
274+
}
275+
)
276+
}
277+
278+
static var generateReleaseNotes: Command {
279+
Command(
280+
description: "Generate release notes",
281+
dependencies: [BrewCommands.ensureGitCliffInstalled],
282+
skipIf: { context in
283+
let arguments = try ReleaseArguments.parse(context.arguments)
284+
try arguments.validate()
285+
286+
let version = arguments.version
287+
let releaseNotesPath = releaseNotesPath(version: version)
288+
if FileManager.default.fileExists(atPath: releaseNotesPath) {
289+
print("Release notes for \(version) already exist at \(releaseNotesPath). Skipping...")
290+
return true
291+
} else {
292+
return false
293+
}
294+
},
295+
run: { context in
296+
let arguments = try ReleaseArguments.parse(context.arguments)
297+
try arguments.validate()
298+
299+
let version = arguments.version
300+
let releaseNotesPath = releaseNotesPath(version: version)
301+
try runAndPrint("git", "cliff", "--latest", "--strip=all", "--tag", version, "--output", releaseNotesPath)
302+
print("Release notes generated at \(releaseNotesPath)")
303+
}
304+
)
305+
}
306+
307+
static var draftReleaseWithArtifacts: Command {
308+
Command(
309+
description: "Draft a release on GitHub",
310+
dependencies: [BrewCommands.ensureGhInstalled],
311+
skipIf: { context in
312+
let arguments = try ReleaseArguments.parse(context.arguments)
313+
try arguments.validate()
314+
315+
let tagName = arguments.version
316+
let ghViewResult = run(bash: "gh release view \(tagName)")
317+
if ghViewResult.succeeded {
318+
print("Release \(tagName) already exists. Skipping...")
319+
return true
320+
} else {
321+
return false
322+
}
323+
},
324+
run: { context in
325+
let arguments = try ReleaseArguments.parse(context.arguments)
326+
try arguments.validate()
327+
328+
print("Drafting release \(arguments.version) on GitHub")
329+
let tagName = arguments.version
330+
let releaseTitle = arguments.version
331+
let draftReleaseCommand =
332+
"gh release create \(tagName) \(Constants.buildArtifactsDirectory)/*.zip --title '\(releaseTitle)' --draft --verify-tag --notes-file \(releaseNotesPath(version: tagName))"
333+
try runAndPrint(bash: draftReleaseCommand)
334+
}
335+
)
336+
}
337+
338+
private static func executableArchivePath(target: BuildTarget, version: String) -> String {
339+
"\(Constants.buildArtifactsDirectory)/\(Constants.executableName)-\(version)-\(target.triple).zip"
340+
}
341+
342+
private static func releaseNotesPath(version: String) -> String {
343+
".build/artifacts/release-notes-\(version).md"
344+
}
345+
}

‎SakeApp/Sakefile.swift

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import ArgumentParser
2+
import Foundation
3+
import Sake
4+
import SwiftShell
5+
6+
@main
7+
@CommandGroup
8+
struct Commands: SakeApp {
9+
public static let configuration = SakeAppConfiguration(
10+
commandGroups: [
11+
TestCommands.self,
12+
ReleaseCommands.self,
13+
]
14+
)
15+
}
16+
17+
@CommandGroup
18+
struct TestCommands {
19+
public static var test: Command {
20+
Command(
21+
description: "Run tests",
22+
dependencies: [ensureDebugBuildIsUpToDate],
23+
run: { context in
24+
try runAndPrint(
25+
bash:
26+
"\(context.projectRoot)/Tests/integration_tests.sh \(context.projectRoot)/.build/debug/progressline"
27+
)
28+
}
29+
)
30+
}
31+
32+
private static var ensureDebugBuildIsUpToDate: Command {
33+
Command(
34+
description: "Ensure debug build is up to date",
35+
run: { context in
36+
try runAndPrint(bash: "swift build --package-path \(context.projectRoot)")
37+
}
38+
)
39+
}
40+
}
41+
42+
extension Command.Context {
43+
var projectRoot: String {
44+
"\(appDirectory)/.."
45+
}
46+
}

‎Sources/ProgressLine.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ struct ProgressLine: AsyncParsableCommand {
88
static let configuration = CommandConfiguration(
99
commandName: "progressline",
1010
abstract: "A command-line tool for compactly tracking the progress of piped commands.",
11-
usage: "some-command | progressline"
11+
usage: "some-command | progressline",
12+
version: progressLineVersion
1213
)
1314

1415
@Option(name: [.long, .customShort("t")], help: "The static text to display instead of the latest stdin data.")

‎Sources/Version.swift

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This file is autogenerated. Do not edit.
2+
let progressLineVersion = "0.2.2"

0 commit comments

Comments
 (0)
Please sign in to comment.