Skip to content
14 changes: 0 additions & 14 deletions Sources/NnexKit/Building/BinaryInfo.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/NnexKit/Building/BinaryOutput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Nikolai Nobadi on 12/10/25.
//

public enum BinaryOutput {
public enum BinaryOutput: Equatable {
public typealias BinaryPath = String

case single(BinaryPath)
Expand Down
16 changes: 16 additions & 0 deletions Sources/NnexKit/Building/BuildResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// BuildResult.swift
// nnex
//
// Created by Nikolai Nobadi on 12/12/25.
//

public struct BuildResult {
public let executableName: String
public let binaryOutput: BinaryOutput

public init(executableName: String, binaryOutput: BinaryOutput) {
self.executableName = executableName
self.binaryOutput = binaryOutput
}
}
20 changes: 13 additions & 7 deletions Sources/NnexKit/Building/ProjectBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public struct ProjectBuilder {

// MARK: - Build
public extension ProjectBuilder {
func build() throws -> BinaryOutput {
func build() throws -> BuildResult {
if !config.skipClean {
try cleanProject()
}
Expand All @@ -32,6 +32,16 @@ public extension ProjectBuilder {
try build(for: arch)
}

let output = try makeBinaryOutpu()

return .init(executableName: config.projectName, binaryOutput: output)
}
}


// MARK: - Private Methods
private extension ProjectBuilder {
func makeBinaryOutpu() throws -> BinaryOutput {
switch config.buildType {
case .arm64, .x86_64:
let arch = config.buildType.archs.first!
Expand All @@ -40,7 +50,7 @@ public extension ProjectBuilder {
try runTests()

return .single(path)

case .universal:
var results: [ReleaseArchitecture: String] = [:]
for arch in config.buildType.archs {
Expand All @@ -52,11 +62,7 @@ public extension ProjectBuilder {
return .multiple(results)
}
}
}


// MARK: - Private Methods
private extension ProjectBuilder {

func log(_ message: String) {
if let progressDelegate = progressDelegate {
progressDelegate.didUpdateProgress(message)
Expand Down
12 changes: 12 additions & 0 deletions Sources/NnexKit/FileSystem/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ public protocol FileSystem {
func readFile(at path: String) throws -> String
func writeFile(at path: String, contents: String) throws
}


// MARK: - Helpers
public extension FileSystem {
func getDirectoryAtPathOrCurrent(path: String?) throws -> any Directory {
guard let path else {
return currentDirectory
}

return try directory(at: path)
}
}
2 changes: 1 addition & 1 deletion Sources/NnexKit/Formula/PublishUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public enum PublishUtilities {
let config = BuildConfig(projectName: formula.name, projectPath: formula.localProjectPath, buildType: buildType, extraBuildArgs: formula.extraBuildArgs, skipClean: false, testCommand: testCommand)
let builder = ProjectBuilder(shell: shell, config: config)

return try builder.build()
return try builder.build().binaryOutput
}

/// Creates tar.gz archives from binary output.
Expand Down
60 changes: 60 additions & 0 deletions Sources/NnexKit/Managers/BuildManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// BuildManager.swift
// nnex
//
// Created by Nikolai Nobadi on 12/12/25.
//

public struct BuildManager {
private let shell: any NnexShell
private let fileSystem: any FileSystem

public init(shell: any NnexShell, fileSystem: any FileSystem) {
self.shell = shell
self.fileSystem = fileSystem
}
}


// MARK: - BuildExecutable
public extension BuildManager {
func buildExecutable(config: BuildConfig, outputLocation: BuildOutputLocation) throws -> BuildResult {
let result = try ProjectBuilder(shell: shell, config: config).build()

switch outputLocation {
case .currentDirectory:
return result
case .desktop:
let desktop = try fileSystem.desktopDirectory()

return try moveToDestination(buildResult: result, destinationPath: desktop.path)
case .custom(let parentPath):
return try moveToDestination(buildResult: result, destinationPath: parentPath)
}
}
}


// MARK: - Private Methods
private extension BuildManager {
func moveToDestination(buildResult: BuildResult, destinationPath: String) throws -> BuildResult {
let executableName = buildResult.executableName

switch buildResult.binaryOutput {
case .single(let path):
let finalPath = destinationPath + "/" + executableName
try shell.runAndPrint(bash: "cp \"\(path)\" \"\(finalPath)\"")
return .init(executableName: executableName, binaryOutput: .single(finalPath))
case .multiple(let binaries):
var results: [ReleaseArchitecture: String] = [:]

for (arch, path) in binaries {
let finalPath = destinationPath + "/" + executableName + "-\(arch.name)"
try shell.runAndPrint(bash: "cp \"\(path)\" \"\(finalPath)\"")
results[arch] = finalPath
}

return .init(executableName: executableName, binaryOutput: .multiple(results))
}
}
}
12 changes: 9 additions & 3 deletions Sources/nnex/Commands/BuildCommand/BuildExecutable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// Created by Nikolai Nobadi on 4/21/25.
//

import ArgumentParser
import NnexKit
import ArgumentParser

extension Nnex {
struct Build: ParsableCommand {
Expand All @@ -32,9 +32,15 @@ extension Nnex {
let context = try Nnex.makeContext()
let fileSystem = Nnex.makeFileSystem()
let buildType = buildType ?? context.loadDefaultBuildType()
let manager = BuildExecutionManager(shell: shell, picker: picker, fileSystem: fileSystem)
let manager = BuildManager(shell: shell, fileSystem: fileSystem)
let folderBrowser = Nnex.makeFolderBrowser(picker: picker, fileSystem: fileSystem)
let controller = BuildController(shell: shell, picker: picker, fileSystem: fileSystem, buildService: manager, folderBrowser: folderBrowser)

try manager.executeBuild(projectPath: path, buildType: buildType, clean: clean, openInFinder: openInFinder)
try controller.buildExecutable(path: path, buildType: buildType, clean: clean, openInFinder: openInFinder)
}
}
}


// MARK: - Extension Dependencies
extension BuildManager: BuildService { }
108 changes: 108 additions & 0 deletions Sources/nnex/Controllers/BuildController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//
// BuildController.swift
// nnex
//
// Created by Nikolai Nobadi on 12/12/25.
//

import NnexKit

struct BuildController {
private let shell: any NnexShell
private let picker: any NnexPicker
private let fileSystem: any FileSystem
private let buildService: any BuildService
private let folderBrowser: any DirectoryBrowser

init(
shell: any NnexShell,
picker: any NnexPicker,
fileSystem: any FileSystem,
buildService: any BuildService,
folderBrowser: any DirectoryBrowser
) {
self.shell = shell
self.picker = picker
self.fileSystem = fileSystem
self.buildService = buildService
self.folderBrowser = folderBrowser
}
}


// MARK: - Actions
extension BuildController {
func buildExecutable(path: String?, buildType: BuildType, clean: Bool, openInFinder: Bool) throws {
let projectFolder = try fileSystem.getDirectoryAtPathOrCurrent(path: path)
let executableName = try getExecutableName(for: projectFolder)
let outputLocation = try selectOutputLocation(buildType: buildType)
let config = BuildConfig(projectName: executableName, projectPath: projectFolder.path, buildType: buildType, extraBuildArgs: [], skipClean: !clean, testCommand: nil)
let result = try buildService.buildExecutable(config: config, outputLocation: outputLocation)

displayBuildResult(result, openInFinder: openInFinder)
}
}


// MARK: - Private Methods
private extension BuildController {
func getExecutableName(for folder: any Directory) throws -> String {
let names = try ExecutableNameResolver.getExecutableNames(from: folder)

guard names.count > 1 else {
return names.first!
}

do {
return try picker.requiredSingleSelection("Which executable would you like to build?", items: names)
} catch {
throw BuildExecutionError.failedToSelectExecutable(reason: error.localizedDescription)
}
}

func selectOutputLocation(buildType: BuildType) throws -> BuildOutputLocation {
let options: [BuildOutputLocation] = [.currentDirectory(buildType), .desktop, .custom("")]
let selection = try picker.requiredSingleSelection("Where would you like to place the built binary?", items: options)

if case .custom = selection {
return try handleCustomLocationInput()
}

return selection
}

func handleCustomLocationInput() throws -> BuildOutputLocation {
let parentFolder = try folderBrowser.browseForDirectory(prompt: "Select the folder where you would like to save the build.")

try picker.requiredPermission(prompt: "The binary will be placed at: \(parentFolder.path). Continue?")

return .custom(parentFolder.path)
}

func displayBuildResult(_ result: BuildResult, openInFinder: Bool) {
switch result.binaryOutput {
case .single(let path):
print("\(result.executableName.lightGreen) was built at \(path)")
openFinder(path: openInFinder ? path : nil)
case .multiple(let binaries):
print("\(result.executableName.lightGreen) builds:".underline)
for (arch, path) in binaries {
print(" \(arch.name): \(path)")
}

openFinder(path: openInFinder ? binaries.values.first : nil)
}
}

func openFinder(path: String?) {
if let path {
try? shell.runAndPrint(bash: "open -R \(path)")
}
}
}


// MARK: - Dependencies
protocol BuildService {
func buildExecutable(config: BuildConfig, outputLocation: BuildOutputLocation) throws -> BuildResult
}
Loading