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
56 changes: 56 additions & 0 deletions .github/workflows/docc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: DocC

on:
push:
branches: ["main"]
workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write

# Allow one concurrent deployment
concurrency:
group: "pages"
cancel-in-progress: true

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Select Xcode 15.2
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.2'

- name: Build DocC
run: |
swift package --allow-writing-to-directory ./docs \
generate-documentation --target LucaCore \
--disable-indexing \
--output-path ./docs \
--transform-for-static-hosting \
--hosting-base-path Luca

# Add redirect from root to documentation
echo '<script>window.location.href += "/documentation/lucacore"</script>' > docs/index.html

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'docs'

- id: deployment
name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ xcuserdata/
# Artifacts
.build/
.luca
docs/
18 changes: 18 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ let package = Package(
.package(url: "https://github.com/apple/swift-argument-parser", exact: "1.6.1"),
.package(url: "https://github.com/apple/swift-crypto.git", exact: "4.0.0"),
.package(url: "https://github.com/tuist/Noora", exact: "0.49.1"),
.package(url: "https://github.com/jpsim/Yams.git", exact: "6.1.0")
.package(url: "https://github.com/jpsim/Yams.git", exact: "6.1.0"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.1.0")
],
targets: [
.executableTarget(
Expand Down
21 changes: 21 additions & 0 deletions Sources/LucaCore/Core/Downloader/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

import Foundation

/// Downloads release archives and executables from remote URLs.
///
/// The `Downloader` handles fetching tool releases from remote servers,
/// supporting both archive formats (ZIP, tar.gz) and standalone executables.
///
/// ## Supported File Types
///
/// - `.zip` - ZIP archives
/// - `.tar.gz` - Gzipped tar archives
/// - No extension - Treated as executable files
///
/// ## Topics
///
/// ### Downloading Files
/// - ``downloadRelease(at:)``
struct Downloader: Downloading {

enum DownloaderError: Error, LocalizedError, Equatable {
Expand All @@ -26,6 +41,12 @@ struct Downloader: Downloading {
self.fileDownloader = fileDownloader
}

/// Downloads a release from the specified URL.
///
/// - Parameter url: The URL to download from.
/// - Returns: A URL to the downloaded file in a temporary location.
/// - Throws: ``DownloaderError/unsupportedFileType(_:)`` if the URL has
/// an unsupported file extension.
func downloadRelease(at url: URL) async throws -> URL {
if SupportedFileTypes.allCases.contains(where: {
url.lastPathComponent.hasSuffix($0.rawValue)
Expand Down
34 changes: 34 additions & 0 deletions Sources/LucaCore/Core/Installer/Installer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,34 @@
import Foundation
import Noora

/// Downloads, verifies, and installs tools from remote URLs.
///
/// The `Installer` orchestrates the complete installation process for development tools:
/// 1. Downloads the release archive or executable from the specified URL
/// 2. Validates the checksum (if provided)
/// 3. Extracts archives or handles executables
/// 4. Validates architecture compatibility
/// 5. Sets executable permissions
/// 6. Creates symlinks in the project's `.luca/active/` directory
///
/// ## Usage
///
/// ```swift
/// let installer = Installer(
/// fileManager: FileManager.default,
/// ignoreArchitectureCheck: false,
/// printer: Printer()
/// )
/// try await installer.install(installationType: .spec(path: lucafilePath))
/// ```
///
/// ## Topics
///
/// ### Installing Tools
/// - ``install(installationType:)``
///
/// ### Installation Types
/// - ``InstallationType``
public struct Installer {

enum InstallerError: Error, LocalizedError {
Expand Down Expand Up @@ -53,6 +81,12 @@ public struct Installer {
self.noora = noora
}

/// Installs tools based on the specified installation type.
///
/// - Parameter installationType: Specifies how to determine which tools to install.
/// Can be either `.spec` to read from a Lucafile, or `.github` to install directly
/// from a GitHub release.
/// - Throws: An error if downloading, extracting, or linking fails.
public func install(installationType: InstallationType) async throws {
if quiet {
try await installQuietly(installationType: installationType)
Expand Down
32 changes: 32 additions & 0 deletions Sources/LucaCore/Core/SpecLoader/SpecLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@
import Foundation
import Yams

/// Loads and parses Lucafile specifications.
///
/// The `SpecLoader` reads YAML-formatted Lucafiles and converts them into
/// ``Spec`` objects that define the tools required for a project.
///
/// ## Lucafile Format
///
/// A Lucafile is a YAML file with the following structure:
///
/// ```yaml
/// ---
/// tools:
/// - name: SwiftLint
/// version: 0.61.0
/// url: https://github.com/realm/SwiftLint/releases/...
/// binaryPath: bin/swiftlint
/// ```
///
/// ## Topics
///
/// ### Loading Specs
/// - ``loadSpec(at:)``
///
/// ### Related Types
/// - ``Spec``
/// - ``Tool``
struct SpecLoader: SpecLoading {

enum SpecLoaderError: Error, LocalizedError {
Expand All @@ -25,6 +51,12 @@ struct SpecLoader: SpecLoading {
self.fileManager = fileManager
}

/// Loads a spec from the specified file path.
///
/// - Parameter path: The URL to the Lucafile.
/// - Returns: A ``Spec`` containing the parsed tool definitions.
/// - Throws: ``SpecLoaderError/missingSpec(_:)`` if the file doesn't exist,
/// or ``SpecLoaderError/invalidSpec(_:_:)`` if the YAML is malformed.
func loadSpec(at path: URL) throws -> Spec {
guard let data = fileManager.contents(atPath: path.path) else {
throw SpecLoaderError.missingSpec(path.path)
Expand Down
19 changes: 19 additions & 0 deletions Sources/LucaCore/Core/SymLinker/SymLinker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

import Foundation

/// Creates and manages symbolic links for installed tools.
///
/// The `SymLinker` is responsible for creating symbolic links in the project's
/// `.luca/active/` directory that point to the actual tool binaries stored
/// in `~/.luca/tools/`.
///
/// This allows projects to reference tools via a consistent path while
/// supporting multiple versions installed globally.
///
/// ## Topics
///
/// ### Creating Symlinks
/// - ``setSymLink(for:)``
struct SymLinker: SymLinking {

enum SymLinkerError: Error, LocalizedError, Equatable {
Expand All @@ -23,6 +36,12 @@ struct SymLinker: SymLinking {

// MARK: - Internal

/// Creates a symbolic link for the specified tool.
///
/// - Parameter tool: The tool to create a symlink for.
/// - Returns: The URL of the created symlink.
/// - Throws: ``SymLinkerError/missingBinaryFile(binaryName:expectedLocation:)``
/// if the binary file doesn't exist at the expected location.
@discardableResult
func setSymLink(for tool: Tool) throws -> URL {
let symLinkFile = fileManager.activeFolder
Expand Down
23 changes: 23 additions & 0 deletions Sources/LucaCore/Core/Unarchiver/Unarchiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

import Foundation

/// Extracts tool archives to their installation destination.
///
/// The `Unarchiver` handles extracting ZIP and tar.gz archives using
/// system utilities (`unzip` and `tar`).
///
/// ## Supported Formats
///
/// - **ZIP** - Extracted using `/usr/bin/unzip`
/// - **tar.gz** - Extracted using `/usr/bin/tar`
///
/// ## Topics
///
/// ### Extracting Archives
/// - ``unarchive(filePath:installationDestination:)``
struct Unarchiver: Unarchiving {

enum UnarchiverError: Error, LocalizedError {
Expand Down Expand Up @@ -29,6 +43,15 @@ struct Unarchiver: Unarchiving {
self.fileTypeDetector = fileTypeDetector
}

/// Extracts an archive to the specified destination.
///
/// - Parameters:
/// - filePath: The path to the archive file.
/// - installationDestination: The directory to extract files into.
/// - Throws: ``UnarchiverError/unrecognisedFileType(_:)`` if the file type
/// cannot be determined, ``UnarchiverError/notAnArchive(_:)`` if the file
/// is an executable, or ``UnarchiverError/failedToUnarchive(_:)`` if
/// extraction fails.
func unarchive(filePath: URL, installationDestination: URL) throws {
try fileManager.createDirectory(at: installationDestination, withIntermediateDirectories: true)

Expand Down
Loading