Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions Sources/NnexKit/Building/BinaryCopyUtility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
// Created by Nikolai Nobadi on 8/26/25.
//

import Files

public struct BinaryCopyUtility {
private let shell: any NnexShell

public init(shell: any NnexShell) {
private let fileSystem: any FileSystem

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

Expand All @@ -23,11 +23,10 @@ public extension BinaryCopyUtility {
switch outputLocation {
case .currentDirectory:
return binaryOutput

case .desktop:
let desktop = try Folder.home.subfolder(named: "Desktop")
return try copyToDestination(binaryOutput: binaryOutput, destinationPath: desktop.path, executableName: executableName)
let desktop = try fileSystem.desktopDirectory()

return try copyToDestination(binaryOutput: binaryOutput, destinationPath: desktop.path, executableName: executableName)
case .custom(let parentPath):
return try copyToDestination(binaryOutput: binaryOutput, destinationPath: parentPath, executableName: executableName)
}
Expand All @@ -39,18 +38,20 @@ public extension BinaryCopyUtility {
private extension BinaryCopyUtility {
func copyToDestination(binaryOutput: BinaryOutput, destinationPath: String, executableName: String) throws -> BinaryOutput {
switch binaryOutput {
case .single(let binaryInfo):
case .single(let path):
let finalPath = destinationPath + "/" + executableName
try shell.runAndPrint(bash: "cp \"\(binaryInfo.path)\" \"\(finalPath)\"")
return .single(.init(path: finalPath))
try shell.runAndPrint(bash: "cp \"\(path)\" \"\(finalPath)\"")

return .single(finalPath)
case .multiple(let binaries):
var results: [ReleaseArchitecture: BinaryInfo] = [:]
for (arch, binaryInfo) in binaries {
var results: [ReleaseArchitecture: String] = [:]

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

return .multiple(results)
}
}
Expand Down
13 changes: 13 additions & 0 deletions Sources/NnexKit/Building/BinaryOutput.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// BinaryOutput.swift
// nnex
//
// Created by Nikolai Nobadi on 12/10/25.
//

public enum BinaryOutput {
public typealias BinaryPath = String

case single(BinaryPath)
case multiple([ReleaseArchitecture: BinaryPath])
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,39 @@
// Created by Nikolai Nobadi on 8/26/25.
//

import Files
import NnexKit

struct ExecutableNameResolver {
func getExecutableNames(from projectFolder: Folder) throws -> [String] {
guard projectFolder.containsFile(named: "Package.swift") else {
throw ExecutableNameResolverError.missingPackageSwift(path: projectFolder.path)
/// Resolves executable names from a Package.swift manifest in a given directory.
public enum ExecutableNameResolver {
/// Extracts executable names from the Package.swift file in the specified directory.
/// - Parameter directory: The directory containing the Package.swift file.
/// - Returns: An array of executable names found in the package manifest.
/// - Throws: `ExecutableNameResolverError` if the manifest is missing, unreadable, empty, or contains no executables.
public static func getExecutableNames(from directory: any Directory) throws -> [String] {
guard directory.containsFile(named: "Package.swift") else {
throw ExecutableNameResolverError.missingPackageSwift(path: directory.path)
}

let content: String
do {
content = try projectFolder.file(named: "Package.swift").readAsString()
content = try directory.readFile(named: "Package.swift")
} catch {
throw ExecutableNameResolverError.failedToReadPackageSwift(reason: error.localizedDescription)
}

guard !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw ExecutableNameResolverError.emptyPackageSwift
}

let names: [String]
do {
names = try ExecutableDetector.getExecutables(packageManifestContent: content)
} catch {
throw ExecutableNameResolverError.failedToParseExecutables(reason: error.localizedDescription)
}

guard !names.isEmpty else {
throw ExecutableNameResolverError.noExecutablesFound
}

return names
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

import Foundation

enum ExecutableNameResolverError: Error, LocalizedError, Equatable {
public enum ExecutableNameResolverError: Error, LocalizedError, Equatable {
case missingPackageSwift(path: String)
case failedToReadPackageSwift(reason: String)
case emptyPackageSwift
case failedToParseExecutables(reason: String)
case noExecutablesFound

var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .missingPackageSwift(let path):
return "No Package.swift file found in '\(path)'. This does not appear to be a Swift package."
Expand All @@ -28,4 +28,4 @@ enum ExecutableNameResolverError: Error, LocalizedError, Equatable {
return "No executable targets found in Package.swift. Make sure your package defines at least one executable product."
}
}
}
}
12 changes: 4 additions & 8 deletions Sources/NnexKit/Building/ProjectBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ public extension ProjectBuilder {

try runTests()

return .single(.init(path: path))
return .single(path)

case .universal:
var results: [ReleaseArchitecture: BinaryInfo] = [:]
var results: [ReleaseArchitecture: String] = [:]
for arch in config.buildType.archs {
let path = binaryPath(for: arch)
results[arch] = .init(path: path)
results[arch] = binaryPath(for: arch)
}

try runTests()
Expand Down Expand Up @@ -139,10 +138,7 @@ private extension ProjectBuilder {


// MARK: - Dependencies
public enum BinaryOutput {
case single(BinaryInfo)
case multiple([ReleaseArchitecture: BinaryInfo])
}


public protocol BuildProgressDelegate: AnyObject {
func didUpdateProgress(_ message: String)
Expand Down
51 changes: 51 additions & 0 deletions Sources/NnexKit/FileSystem/DefaultFileSystem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// DefaultFileSystem.swift
// nnex
//
// Created by Nikolai Nobadi on 12/9/25.
//

import Files
import Foundation

public struct DefaultFileSystem {
private let fileManager: FileManager

public init(fileManager: FileManager = .default) {
self.fileManager = fileManager
}
}


// MARK: - FileSystem
extension DefaultFileSystem: FileSystem {
public var homeDirectory: any Directory {
return FilesDirectoryAdapter(folder: Folder.home)
}

public var currentDirectory: any Directory {
return FilesDirectoryAdapter(folder: Folder.current)
}

public func directory(at path: String) throws -> any Directory {
return try FilesDirectoryAdapter(folder: Folder(path: path))
}

public func desktopDirectory() throws -> any Directory {
let desktopPath = fileManager.homeDirectoryForCurrentUser.appending(path: "Desktop").path()
return try directory(at: desktopPath)
}

public func readFile(at path: String) throws -> String {
return try String(contentsOfFile: path, encoding: .utf8)
}

public func writeFile(at path: String, contents: String) throws {
try contents.write(toFile: path, atomically: true, encoding: .utf8)
}

public func moveToTrash(at path: String) throws {
try fileManager.trashItem(at: .init(fileURLWithPath: path), resultingItemURL: nil)
}
}

23 changes: 23 additions & 0 deletions Sources/NnexKit/FileSystem/Directory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Directory.swift
// nnex
//
// Created by Nikolai Nobadi on 12/9/25.
//

public protocol Directory {
var path: String { get }
var name: String { get }
var `extension`: String? { get }
var subdirectories: [any Directory] { get }

func move(to parent: any Directory) throws
func containsFile(named name: String) -> Bool
func subdirectory(named name: String) throws -> any Directory
func createSubdirectory(named name: String) throws -> any Directory
func createSubfolderIfNeeded(named name: String) throws -> any Directory
func deleteFile(named name: String) throws
func createFile(named name: String, contents: String) throws -> String
func readFile(named name: String) throws -> String
func findFiles(withExtension extension: String?, recursive: Bool) throws -> [String]
}
17 changes: 17 additions & 0 deletions Sources/NnexKit/FileSystem/FileSystem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// FileSystem.swift
// nnex
//
// Created by Nikolai Nobadi on 12/9/25.
//

public protocol FileSystem {
var homeDirectory: any Directory { get }
var currentDirectory: any Directory { get }

func moveToTrash(at path: String) throws
func directory(at path: String) throws -> any Directory
func desktopDirectory() throws -> any Directory
func readFile(at path: String) throws -> String
func writeFile(at path: String, contents: String) throws
}
10 changes: 10 additions & 0 deletions Sources/NnexKit/FileSystem/FileSystemError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// FileSystemError.swift
// nnex
//
// Created by Nikolai Nobadi on 12/9/25.
//

enum FileSystemError: Error {
case incompatibleDirectory
}
90 changes: 90 additions & 0 deletions Sources/NnexKit/FileSystem/FilesDirectoryAdapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// FilesDirectoryAdapter.swift
// nnex
//
// Created by Nikolai Nobadi on 12/9/25.
//

import Files

public struct FilesDirectoryAdapter {
private let folder: Folder

public init(folder: Folder) {
self.folder = folder
}
}


// MARK: - Directory
extension FilesDirectoryAdapter: Directory {
public var path: String {
return folder.path
}

public var name: String {
return folder.name
}

public var `extension`: String? {
return folder.extension
}

public var subdirectories: [any Directory] {
return folder.subfolders.map(FilesDirectoryAdapter.init)
}

public func containsFile(named name: String) -> Bool {
return folder.containsFile(named: name)
}

public func subdirectory(named name: String) throws -> any Directory {
return try FilesDirectoryAdapter(folder: folder.subfolder(named: name))
}

public func createSubdirectory(named name: String) throws -> any Directory {
if let existing = try? folder.subfolder(named: name) {
return FilesDirectoryAdapter(folder: existing)
}

return try FilesDirectoryAdapter(folder: folder.createSubfolder(named: name))
}

public func move(to parent: any Directory) throws {
guard let destination = (parent as? FilesDirectoryAdapter)?.folder else {
throw FileSystemError.incompatibleDirectory
}

try folder.move(to: destination)
}

public func createSubfolderIfNeeded(named name: String) throws -> any Directory {
return try FilesDirectoryAdapter(folder: folder.createSubfolderIfNeeded(withName: name))
}

public func deleteFile(named name: String) throws {
try folder.file(named: name).delete()
}

public func createFile(named name: String, contents: String) throws -> String {
let file = try folder.createFile(named: name)
try file.write(contents)
return file.path
}

public func readFile(named name: String) throws -> String {
return try folder.file(named: name).readAsString()
}

public func findFiles(withExtension extension: String?, recursive: Bool) throws -> [String] {
let fileSequence = recursive ? folder.files.recursive : folder.files
let files = Array(fileSequence)

if let ext = `extension` {
return files.filter { $0.extension == ext }.map { $0.path }
}

return files.map { $0.path }
}
}

Loading