-
-
Notifications
You must be signed in to change notification settings - Fork 11
Bug fixes, unit tests, and SwiftArgumentParser migration #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f27299e
6c44053
8f7b5c8
ee3c4e4
fa7f115
a0ea75c
97f00ce
b666326
a940d0b
4b37bf4
a156fa4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| ## Project Overview | ||
|
|
||
| AckGen is an **Ack**nowledgements **Gen**erator that automatically extracts license information from Swift Package Manager dependencies and generates plist files for iOS/macOS apps. | ||
|
|
||
| ## Build Commands | ||
|
|
||
| ```bash | ||
| # Build the CLI executable | ||
| swift build | ||
|
|
||
| # Build in release mode | ||
| swift build -c release | ||
|
|
||
| # Run the CLI (requires Xcode environment variables SRCROOT, PROJECT_TEMP_DIR) | ||
| swift run ackgen --output <output_path> --settings --title <settingsTitle> | ||
|
|
||
| # Run tests | ||
| swift test | ||
|
|
||
| # Bootstrap example project (requires mint) | ||
| make bootstrap | ||
|
|
||
| # Open example project in Xcode | ||
| make open | ||
| ``` | ||
|
|
||
| ## Architecture | ||
|
|
||
| Three-module Swift Package with one external dependency (`swift-argument-parser` for the CLI): | ||
|
|
||
| ``` | ||
| AckGenCore (library) | ||
| βββ Acknowledgement.swift # Model + plist decoding for runtime use | ||
| β | ||
| AckGenCLI (executable: ackgen) | ||
| βββ AckGen.swift # Main logic: scans SourcePackages/checkouts for LICENSE files | ||
| βββ AcknowledgementsStringsTable.swift # Settings.bundle plist format | ||
| β | ||
| AckGenUI (library) | ||
| βββ AcknowledgementsList.swift # SwiftUI list view for displaying licenses | ||
| ``` | ||
|
|
||
| **Data Flow:** | ||
| 1. CLI runs as Xcode build phase, reads `PROJECT_TEMP_DIR` to find SPM checkouts | ||
| 2. Scans for LICENSE, LICENSE.txt, LICENSE.md in each package directory | ||
| 3. Encodes to plist (standard array or Settings.bundle format) | ||
| 4. App uses `Acknowledgement.all()` to decode plist at runtime | ||
|
|
||
| **Two Output Formats:** | ||
| - Standard: Array of `Acknowledgement` (default) | ||
| - Settings Bundle: `AcknowledgementsStringsTable` format with `StringsTable` and `PreferenceSpecifiers` keys | ||
|
|
||
| ## Example App | ||
|
|
||
| Located in `Example/`. Uses XcodeGen (`project.yml`) for project generation. Contains pre-build scripts demonstrating both plist formats: | ||
| - `ackgen.sh` - Standard format | ||
| - `ackgen_settings.sh` - Settings.bundle format | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,66 +1,82 @@ | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // AckGen.swift | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // | ||||||||||||||||||||
| // Created by Martin Pfundmair on 2021-08-09. | ||||||||||||||||||||
| // | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import AckGenCore | ||||||||||||||||||||
| import ArgumentParser | ||||||||||||||||||||
| import Foundation | ||||||||||||||||||||
|
|
||||||||||||||||||||
| struct AckGenCLI { | ||||||||||||||||||||
| @main | ||||||||||||||||||||
| struct AckGen: ParsableCommand { | ||||||||||||||||||||
| static let configuration = CommandConfiguration( | ||||||||||||||||||||
| abstract: "Generate acknowledgements plist from Swift Package Manager dependencies", | ||||||||||||||||||||
| version: "0.8.0" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Option(name: .shortAndLong, help: "Output path for the generated plist file") | ||||||||||||||||||||
| var output: String? | ||||||||||||||||||||
|
|
||||||||||||||||||||
| @Flag(name: .long, help: "Generate Settings.bundle format") | ||||||||||||||||||||
| var settings: Bool = false | ||||||||||||||||||||
|
|
||||||||||||||||||||
| static func main() { | ||||||||||||||||||||
| @Option(name: .long, help: "Title for Settings.bundle (only used with --settings)") | ||||||||||||||||||||
| var title: String = "Acknowledgements" | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+12
to
+27
|
||||||||||||||||||||
| func run() throws { | ||||||||||||||||||||
| print("Generating Acknowledgements file") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let licenseFiles: [String] = ["LICENSE", "LICENSE.txt", "LICENSE.md"] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let arguments: [String] = Array(CommandLine.arguments.dropFirst()) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| guard let srcRoot = ProcessInfo.processInfo.environment["SRCROOT"] else { | ||||||||||||||||||||
| print("error: could not detect the source root directory.") | ||||||||||||||||||||
| return | ||||||||||||||||||||
| throw ValidationError("Could not detect the source root directory (SRCROOT environment variable not set)") | ||||||||||||||||||||
| } | ||||||||||||||||||||
| guard let tempDirPath = ProcessInfo.processInfo.environment["PROJECT_TEMP_DIR"] else { | ||||||||||||||||||||
| print("error: could not detect the project's temp directory.") | ||||||||||||||||||||
| return | ||||||||||||||||||||
| throw ValidationError("Could not detect the project's temp directory (PROJECT_TEMP_DIR environment variable not set)") | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let plistPath: String = arguments.first ?? "\(srcRoot)/Acknowledgements.plist" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let forSettings: Bool = arguments.count > 1 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let settingsTitle: String = arguments.count > 2 ? arguments[2] : "Acknowledgements" | ||||||||||||||||||||
| let plistPath: String = output ?? "\(srcRoot)/Acknowledgements.plist" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let packageCachePath = tempDirPath.components(separatedBy: "/Build/")[0] + "/SourcePackages/checkouts" | ||||||||||||||||||||
|
||||||||||||||||||||
| let packageCachePath = tempDirPath.components(separatedBy: "/Build/")[0] + "/SourcePackages/checkouts" | |
| guard let buildRange = tempDirPath.range(of: "/Build/", options: .backwards) else { | |
| throw ValidationError( | |
| "Could not determine DerivedData root from PROJECT_TEMP_DIR (\(tempDirPath)). " + | |
| "Expected path to contain a '/Build/' component." | |
| ) | |
| } | |
| let derivedDataRoot = String(tempDirPath[..<buildRange.lowerBound]) | |
| let packageCachePath = derivedDataRoot + "/SourcePackages/checkouts" |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new CLI behaviors (skipping invalid UTF-8 license files with a warning, and failing with ValidationError when no licenses are found) arenβt covered by tests. Since the repo now has a working XCTest suite, consider adding at least a minimal CLI test (or extracting the scanning/encoding logic into AckGenCore for unit testing) to prevent regressions in build-phase behavior.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // | ||
| // AcknowledgementAllTests.swift | ||
| // AckGenTests | ||
| // | ||
| // Created by Martin Pfundmair on 2026-01-13. | ||
| // | ||
|
|
||
| import XCTest | ||
| @testable import AckGenCore | ||
|
|
||
| final class AcknowledgementAllTests: XCTestCase { | ||
|
|
||
| func testAllDecodesFixturePlist() { | ||
| // Given: A fixture plist in the test bundle's Fixtures directory | ||
| // When: Loading acknowledgements from the fixture | ||
| let acks = Acknowledgement.all(fromPlist: "Fixtures/Acknowledgements", in: Bundle.module) | ||
|
|
||
| // Then: All entries should be decoded correctly | ||
| XCTAssertEqual(acks.count, 3) | ||
|
|
||
| // Verify the content is decoded properly | ||
| let titles = acks.map(\.title) | ||
| XCTAssertTrue(titles.contains("Zebra")) | ||
| XCTAssertTrue(titles.contains("apple")) | ||
| XCTAssertTrue(titles.contains("Banana")) | ||
|
|
||
| // Verify license content is present | ||
| let zebra = acks.first { $0.title == "Zebra" } | ||
| XCTAssertEqual(zebra?.license, "MIT License for Zebra package") | ||
| } | ||
|
|
||
| func testAllSortsCaseInsensitively() { | ||
| // Given: A fixture plist with mixed case titles (Zebra, apple, Banana) | ||
| // When: Loading acknowledgements (which applies case-insensitive sorting) | ||
| let acks = Acknowledgement.all(fromPlist: "Fixtures/Acknowledgements", in: Bundle.module) | ||
|
|
||
| // Then: Should be sorted case-insensitively: apple, Banana, Zebra | ||
| XCTAssertEqual(acks.count, 3) | ||
| XCTAssertEqual(acks[0].title, "apple") // 'a' comes first | ||
| XCTAssertEqual(acks[1].title, "Banana") // 'B' (as 'b') comes second | ||
| XCTAssertEqual(acks[2].title, "Zebra") // 'Z' (as 'z') comes last | ||
| } | ||
|
|
||
| func testAllReturnsEmptyArrayForMissingPlist() { | ||
| // Given: A non-existent plist name | ||
| let bundle = Bundle.module | ||
|
|
||
| // When: Loading from a non-existent plist | ||
| let acks = Acknowledgement.all(fromPlist: "NonExistent", in: bundle) | ||
|
|
||
| // Then: Should return empty array instead of crashing | ||
| XCTAssertTrue(acks.isEmpty) | ||
| } | ||
|
|
||
| func testAllReturnsEmptyArrayForInvalidPlist() { | ||
| // Given: A malformed/invalid plist file (fixture: invalid-utf8-license) | ||
| // Note: This test verifies graceful handling of decode failures for malformed plist data | ||
| // The all() method should return an empty array for any plist decode failure | ||
|
|
||
| // When: Attempting to decode a malformed plist that cannot be parsed | ||
| let acks = Acknowledgement.all(fromPlist: "Fixtures/invalid-utf8-license", in: Bundle.module) | ||
|
|
||
|
Comment on lines
+55
to
+62
|
||
| // Then: Should return empty array instead of crashing | ||
| XCTAssertTrue(acks.isEmpty) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CLI invocation documented here still uses the old positional-argument interface (
swift run ackgen [output_path] [forSettings] [settingsTitle]). Since the CLI was migrated to SwiftArgumentParser, this should be updated to the new flags (--output,--settings,--title) to avoid misleading contributors.