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
12 changes: 10 additions & 2 deletions Sources/LucaCLI/Commands/InstallCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,21 @@ luca install

@Flag(help: "Skip architecture compatibility validation during installation.")
var ignoreArchCheck: Bool = false

@Flag(help: "Suppress all output except final success message.")
var quiet: Bool = false

func run() async throws {
let printer = Printer()
let printer: Printing = quiet ? QuietPrinter() : Printer()
Header(printer: printer).printHeader()

let fileManager = FileManagerWrapper(fileManager: .default)
let installer = Installer(fileManager: fileManager, ignoreArchitectureCheck: ignoreArchCheck, printer: printer)
let installer = Installer(
fileManager: fileManager,
ignoreArchitectureCheck: ignoreArchCheck,
quiet: quiet,
printer: printer
)
let arguments = Arguments(
spec: spec,
identifier: identifier,
Expand Down
2 changes: 1 addition & 1 deletion Sources/LucaCLI/Utils/Header.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import LucaCore

struct Header {

let printer: Printer
let printer: Printing

func printHeader() {
let asciiHeader = """
Expand Down
58 changes: 50 additions & 8 deletions Sources/LucaCore/Core/Installer/Installer.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Installer.swift

import Foundation
import Noora

public struct Installer {

Expand All @@ -26,12 +27,16 @@ public struct Installer {
private let linkedToolsLister: LinkedToolsLister
private let unlinker: Unlinker
private let ignoreArchitectureCheck: Bool
private let quiet: Bool
private let noora: Noorable

public init(
fileManager: FileManaging,
ignoreArchitectureCheck: Bool,
quiet: Bool = false,
printer: Printing,
downloader: Downloading? = nil
downloader: Downloading? = nil,
noora: Noorable = Noora()
) {
self.fileManager = fileManager
self.printer = printer
Expand All @@ -44,9 +49,52 @@ public struct Installer {
self.linkedToolsLister = LinkedToolsLister(fileManager: fileManager)
self.unlinker = Unlinker(fileManager: fileManager, printer: printer)
self.ignoreArchitectureCheck = ignoreArchitectureCheck
self.quiet = quiet
self.noora = noora
}

public func install(installationType: InstallationType) async throws {
if quiet {
try await installQuietly(installationType: installationType)
} else {
try await installVerbose(installationType: installationType)
}
}

// MARK: - Private

private func installQuietly(installationType: InstallationType) async throws {
try await noora.progressStep(
message: "Installing tools",
successMessage: "Tools have been installed for the current project",
errorMessage: "Failed to install tools",
showSpinner: true
) { updateMessage in
let dataDownloader = DataDownloader(session: .shared)
let releaseInfoProvider = ReleaseInfoProvider(dataDownloader: dataDownloader)
let specLoader = SpecLoader(fileManager: .default)
let toolFactory = ToolFactory(releaseInfoProvider: releaseInfoProvider, specLoader: specLoader)

updateMessage("Detecting tools to install")
let tools = try await toolFactory.toolsForInstallationType(installationType)

// Unlink orphaned tools only when installing from a spec
if case .spec = installationType {
try unlinkOrphanedTools(specTools: tools)
}

for tool in tools {
updateMessage("Installing \(tool.name) \(tool.version)")
if isToolInstalled(tool) {
try reinstall(tool)
} else {
try await install(tool)
}
}
}
}

private func installVerbose(installationType: InstallationType) async throws {
let dataDownloader = DataDownloader(session: .shared)
let releaseInfoProvider = ReleaseInfoProvider(dataDownloader: dataDownloader)
let specLoader = SpecLoader(fileManager: .default)
Expand All @@ -64,13 +112,7 @@ public struct Installer {

printer.printFormatted("\(.info("🏃‍♂️ Installing tools for the current project."))")
printer.printFormatted("")

try await installTools(tools)
}

// MARK: - Private

private func installTools(_ tools: [Tool]) async throws {

for tool in tools {
if isToolInstalled(tool) {
try reinstall(tool)
Expand Down
11 changes: 11 additions & 0 deletions Sources/LucaCore/Core/Printer/QuietPrinter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// QuietPrinter.swift

import Foundation
import Noora

public struct QuietPrinter: Printing {

public init() {}

public func printFormatted(_ terminalText: TerminalText) {}
}
69 changes: 35 additions & 34 deletions Tests/Core/InstallerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@ struct InstallerTests {
downloader = DownloaderMock(result: .fixture(Fixture(filename: "MockRelease", type: "zip")))
}

private func makeInstaller() -> Installer {
private func makeInstaller(quiet: Bool) -> Installer {
Installer(
fileManager: fileManager,
ignoreArchitectureCheck: true,
quiet: quiet,
printer: PrinterMock(),
downloader: downloader
)
}

@Test
func test_installIndividuals() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_installIndividuals(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

let fixture = Fixture(filename: "Lucafile_mock", type: "yml")
let bundle = Bundle.module
Expand Down Expand Up @@ -73,9 +74,9 @@ struct InstallerTests {
}
}

@Test
func test_installSpec() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_installSpec(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

let fixture = Fixture(filename: "Lucafile_mock", type: "yml")
let bundle = Bundle.module
Expand Down Expand Up @@ -109,9 +110,9 @@ struct InstallerTests {
#expect(fileManager.fileExists(atPath: toolSymLink.path))
}

@Test
func test_installInvalid() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_installInvalid(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

let fixture = Fixture(filename: "Lucafile_invalid", type: "yml")
let bundle = Bundle.module
Expand All @@ -122,9 +123,9 @@ struct InstallerTests {
}
}

@Test
func test_reinstallSpec() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_reinstallSpec(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

let fixture = Fixture(filename: "Lucafile_mock", type: "yml")
let bundle = Bundle.module
Expand Down Expand Up @@ -154,9 +155,9 @@ struct InstallerTests {
}
}

@Test
func test_installToolUpgradeVersion() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_installToolUpgradeVersion(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

let lowVersionFixture = Fixture(filename: "Lucafile_mock_lowversion", type: "yml")
let highVersionFixture = Fixture(filename: "Lucafile_mock_highversion", type: "yml")
Expand Down Expand Up @@ -188,9 +189,9 @@ struct InstallerTests {
}
}

@Test
func test_installToolDowngradeVersion() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_installToolDowngradeVersion(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

let lowVersionFixture = Fixture(filename: "Lucafile_mock_lowversion", type: "yml")
let highVersionFixture = Fixture(filename: "Lucafile_mock_highversion", type: "yml")
Expand Down Expand Up @@ -222,9 +223,9 @@ struct InstallerTests {
}
}

@Test
func test_installSpec_unlinksOrphanedTools() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_installSpec_unlinksOrphanedTools(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

// First, install the full spec with all tools
let fullFixture = Fixture(filename: "Lucafile_mock", type: "yml")
Expand Down Expand Up @@ -258,9 +259,9 @@ struct InstallerTests {
}
}

@Test
func test_installIndividual_doesNotUnlinkExistingTools() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_installIndividual_doesNotUnlinkExistingTools(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

// First, install a spec with multiple tools
let fullFixture = Fixture(filename: "Lucafile_mock", type: "yml")
Expand Down Expand Up @@ -303,9 +304,9 @@ struct InstallerTests {
#expect(fileManager.fileExists(atPath: individualToolPath.path))
}

@Test
func test_installSpec_noOrphanedTools() async throws {
let installer = makeInstaller()
@Test(arguments: [true, false])
func test_installSpec_noOrphanedTools(quiet: Bool) async throws {
let installer = makeInstaller(quiet: quiet)

// Install a spec
let fixture = Fixture(filename: "Lucafile_mock", type: "yml")
Expand All @@ -327,12 +328,12 @@ struct InstallerTests {
}
}

@Test
func test_installSpec_sameToolDifferentVersion() async throws {
@Test(arguments: [true, false])
func test_installSpec_sameToolDifferentVersion(quiet: Bool) async throws {
// This test verifies that installing the same tool with different versions
// correctly updates the symlink to point to the new version.

let installer = makeInstaller()
let installer = makeInstaller(quiet: quiet)

// Install MockTool version 1.0.0
let lowVersionFixture = Fixture(filename: "Lucafile_mock_lowversion", type: "yml")
Expand All @@ -352,12 +353,12 @@ struct InstallerTests {
#expect(fileManager.fileExists(atPath: symLink.path))
}

@Test
func test_installSpec_unlinksToolsByName() async throws {
@Test(arguments: [true, false])
func test_installSpec_unlinksToolsByName(quiet: Bool) async throws {
// This test verifies that orphan detection correctly identifies tools to unlink
// by comparing tool names (from folder structure) against spec tool names.

let installer = makeInstaller()
let installer = makeInstaller(quiet: quiet)

// First, install a spec with MockTool
let mockToolFixture = Fixture(filename: "Lucafile_mock_lowversion", type: "yml")
Expand Down