Skip to content

Commit 89e2ee3

Browse files
Merge pull request #46 from nikolainobadi/new-build-command
New build command
2 parents 038a7be + 38feb70 commit 89e2ee3

File tree

15 files changed

+561
-438
lines changed

15 files changed

+561
-438
lines changed

Sources/NnexKit/Building/BinaryInfo.swift

Lines changed: 0 additions & 14 deletions
This file was deleted.

Sources/NnexKit/Building/BinaryOutput.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// Created by Nikolai Nobadi on 12/10/25.
66
//
77

8-
public enum BinaryOutput {
8+
public enum BinaryOutput: Equatable {
99
public typealias BinaryPath = String
1010

1111
case single(BinaryPath)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// BuildResult.swift
3+
// nnex
4+
//
5+
// Created by Nikolai Nobadi on 12/12/25.
6+
//
7+
8+
public struct BuildResult {
9+
public let executableName: String
10+
public let binaryOutput: BinaryOutput
11+
12+
public init(executableName: String, binaryOutput: BinaryOutput) {
13+
self.executableName = executableName
14+
self.binaryOutput = binaryOutput
15+
}
16+
}

Sources/NnexKit/Building/ProjectBuilder.swift

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public struct ProjectBuilder {
2323

2424
// MARK: - Build
2525
public extension ProjectBuilder {
26-
func build() throws -> BinaryOutput {
26+
func build() throws -> BuildResult {
2727
if !config.skipClean {
2828
try cleanProject()
2929
}
@@ -32,6 +32,16 @@ public extension ProjectBuilder {
3232
try build(for: arch)
3333
}
3434

35+
let output = try makeBinaryOutpu()
36+
37+
return .init(executableName: config.projectName, binaryOutput: output)
38+
}
39+
}
40+
41+
42+
// MARK: - Private Methods
43+
private extension ProjectBuilder {
44+
func makeBinaryOutpu() throws -> BinaryOutput {
3545
switch config.buildType {
3646
case .arm64, .x86_64:
3747
let arch = config.buildType.archs.first!
@@ -40,7 +50,7 @@ public extension ProjectBuilder {
4050
try runTests()
4151

4252
return .single(path)
43-
53+
4454
case .universal:
4555
var results: [ReleaseArchitecture: String] = [:]
4656
for arch in config.buildType.archs {
@@ -52,11 +62,7 @@ public extension ProjectBuilder {
5262
return .multiple(results)
5363
}
5464
}
55-
}
56-
57-
58-
// MARK: - Private Methods
59-
private extension ProjectBuilder {
65+
6066
func log(_ message: String) {
6167
if let progressDelegate = progressDelegate {
6268
progressDelegate.didUpdateProgress(message)

Sources/NnexKit/FileSystem/FileSystem.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,15 @@ public protocol FileSystem {
1515
func readFile(at path: String) throws -> String
1616
func writeFile(at path: String, contents: String) throws
1717
}
18+
19+
20+
// MARK: - Helpers
21+
public extension FileSystem {
22+
func getDirectoryAtPathOrCurrent(path: String?) throws -> any Directory {
23+
guard let path else {
24+
return currentDirectory
25+
}
26+
27+
return try directory(at: path)
28+
}
29+
}

Sources/NnexKit/Formula/PublishUtilities.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public enum PublishUtilities {
1919
let config = BuildConfig(projectName: formula.name, projectPath: formula.localProjectPath, buildType: buildType, extraBuildArgs: formula.extraBuildArgs, skipClean: false, testCommand: testCommand)
2020
let builder = ProjectBuilder(shell: shell, config: config)
2121

22-
return try builder.build()
22+
return try builder.build().binaryOutput
2323
}
2424

2525
/// Creates tar.gz archives from binary output.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// BuildManager.swift
3+
// nnex
4+
//
5+
// Created by Nikolai Nobadi on 12/12/25.
6+
//
7+
8+
public struct BuildManager {
9+
private let shell: any NnexShell
10+
private let fileSystem: any FileSystem
11+
12+
public init(shell: any NnexShell, fileSystem: any FileSystem) {
13+
self.shell = shell
14+
self.fileSystem = fileSystem
15+
}
16+
}
17+
18+
19+
// MARK: - BuildExecutable
20+
public extension BuildManager {
21+
func buildExecutable(config: BuildConfig, outputLocation: BuildOutputLocation) throws -> BuildResult {
22+
let result = try ProjectBuilder(shell: shell, config: config).build()
23+
24+
switch outputLocation {
25+
case .currentDirectory:
26+
return result
27+
case .desktop:
28+
let desktop = try fileSystem.desktopDirectory()
29+
30+
return try moveToDestination(buildResult: result, destinationPath: desktop.path)
31+
case .custom(let parentPath):
32+
return try moveToDestination(buildResult: result, destinationPath: parentPath)
33+
}
34+
}
35+
}
36+
37+
38+
// MARK: - Private Methods
39+
private extension BuildManager {
40+
func moveToDestination(buildResult: BuildResult, destinationPath: String) throws -> BuildResult {
41+
let executableName = buildResult.executableName
42+
43+
switch buildResult.binaryOutput {
44+
case .single(let path):
45+
let finalPath = destinationPath + "/" + executableName
46+
try shell.runAndPrint(bash: "cp \"\(path)\" \"\(finalPath)\"")
47+
return .init(executableName: executableName, binaryOutput: .single(finalPath))
48+
case .multiple(let binaries):
49+
var results: [ReleaseArchitecture: String] = [:]
50+
51+
for (arch, path) in binaries {
52+
let finalPath = destinationPath + "/" + executableName + "-\(arch.name)"
53+
try shell.runAndPrint(bash: "cp \"\(path)\" \"\(finalPath)\"")
54+
results[arch] = finalPath
55+
}
56+
57+
return .init(executableName: executableName, binaryOutput: .multiple(results))
58+
}
59+
}
60+
}

Sources/nnex/Commands/BuildCommand/BuildExecutable.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
// Created by Nikolai Nobadi on 4/21/25.
66
//
77

8-
import ArgumentParser
98
import NnexKit
9+
import ArgumentParser
1010

1111
extension Nnex {
1212
struct Build: ParsableCommand {
@@ -32,9 +32,15 @@ extension Nnex {
3232
let context = try Nnex.makeContext()
3333
let fileSystem = Nnex.makeFileSystem()
3434
let buildType = buildType ?? context.loadDefaultBuildType()
35-
let manager = BuildExecutionManager(shell: shell, picker: picker, fileSystem: fileSystem)
35+
let manager = BuildManager(shell: shell, fileSystem: fileSystem)
36+
let folderBrowser = Nnex.makeFolderBrowser(picker: picker, fileSystem: fileSystem)
37+
let controller = BuildController(shell: shell, picker: picker, fileSystem: fileSystem, buildService: manager, folderBrowser: folderBrowser)
3638

37-
try manager.executeBuild(projectPath: path, buildType: buildType, clean: clean, openInFinder: openInFinder)
39+
try controller.buildExecutable(path: path, buildType: buildType, clean: clean, openInFinder: openInFinder)
3840
}
3941
}
4042
}
43+
44+
45+
// MARK: - Extension Dependencies
46+
extension BuildManager: BuildService { }
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
//
2+
// BuildController.swift
3+
// nnex
4+
//
5+
// Created by Nikolai Nobadi on 12/12/25.
6+
//
7+
8+
import NnexKit
9+
10+
struct BuildController {
11+
private let shell: any NnexShell
12+
private let picker: any NnexPicker
13+
private let fileSystem: any FileSystem
14+
private let buildService: any BuildService
15+
private let folderBrowser: any DirectoryBrowser
16+
17+
init(
18+
shell: any NnexShell,
19+
picker: any NnexPicker,
20+
fileSystem: any FileSystem,
21+
buildService: any BuildService,
22+
folderBrowser: any DirectoryBrowser
23+
) {
24+
self.shell = shell
25+
self.picker = picker
26+
self.fileSystem = fileSystem
27+
self.buildService = buildService
28+
self.folderBrowser = folderBrowser
29+
}
30+
}
31+
32+
33+
// MARK: - Actions
34+
extension BuildController {
35+
func buildExecutable(path: String?, buildType: BuildType, clean: Bool, openInFinder: Bool) throws {
36+
let projectFolder = try fileSystem.getDirectoryAtPathOrCurrent(path: path)
37+
let executableName = try getExecutableName(for: projectFolder)
38+
let outputLocation = try selectOutputLocation(buildType: buildType)
39+
let config = BuildConfig(projectName: executableName, projectPath: projectFolder.path, buildType: buildType, extraBuildArgs: [], skipClean: !clean, testCommand: nil)
40+
let result = try buildService.buildExecutable(config: config, outputLocation: outputLocation)
41+
42+
displayBuildResult(result, openInFinder: openInFinder)
43+
}
44+
}
45+
46+
47+
// MARK: - Private Methods
48+
private extension BuildController {
49+
func getExecutableName(for folder: any Directory) throws -> String {
50+
let names = try ExecutableNameResolver.getExecutableNames(from: folder)
51+
52+
guard names.count > 1 else {
53+
return names.first!
54+
}
55+
56+
do {
57+
return try picker.requiredSingleSelection("Which executable would you like to build?", items: names)
58+
} catch {
59+
throw BuildExecutionError.failedToSelectExecutable(reason: error.localizedDescription)
60+
}
61+
}
62+
63+
func selectOutputLocation(buildType: BuildType) throws -> BuildOutputLocation {
64+
let options: [BuildOutputLocation] = [.currentDirectory(buildType), .desktop, .custom("")]
65+
let selection = try picker.requiredSingleSelection("Where would you like to place the built binary?", items: options)
66+
67+
if case .custom = selection {
68+
return try handleCustomLocationInput()
69+
}
70+
71+
return selection
72+
}
73+
74+
func handleCustomLocationInput() throws -> BuildOutputLocation {
75+
let parentFolder = try folderBrowser.browseForDirectory(prompt: "Select the folder where you would like to save the build.")
76+
77+
try picker.requiredPermission(prompt: "The binary will be placed at: \(parentFolder.path). Continue?")
78+
79+
return .custom(parentFolder.path)
80+
}
81+
82+
func displayBuildResult(_ result: BuildResult, openInFinder: Bool) {
83+
switch result.binaryOutput {
84+
case .single(let path):
85+
print("\(result.executableName.lightGreen) was built at \(path)")
86+
openFinder(path: openInFinder ? path : nil)
87+
case .multiple(let binaries):
88+
print("\(result.executableName.lightGreen) builds:".underline)
89+
for (arch, path) in binaries {
90+
print(" \(arch.name): \(path)")
91+
}
92+
93+
openFinder(path: openInFinder ? binaries.values.first : nil)
94+
}
95+
}
96+
97+
func openFinder(path: String?) {
98+
if let path {
99+
try? shell.runAndPrint(bash: "open -R \(path)")
100+
}
101+
}
102+
}
103+
104+
105+
// MARK: - Dependencies
106+
protocol BuildService {
107+
func buildExecutable(config: BuildConfig, outputLocation: BuildOutputLocation) throws -> BuildResult
108+
}

0 commit comments

Comments
 (0)