diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6aaf924 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build and Test + runs-on: macos-15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate BuildInfo.swift + run: ./Scripts/generate-build-info.sh + + - name: Swift Build (SPM) + run: swift build + + - name: Install SwiftLint + run: brew install swiftlint + + - name: SwiftLint + run: swiftlint lint --strict + + - name: Swift Test (SPM) + run: swift test + + - name: Build LogoScreenSaver.saver (Xcode) + run: | + xcodebuild build \ + -project LogoScreenSaver.xcodeproj \ + -scheme LogoScreenSaver \ + -configuration Debug \ + -destination 'platform=macOS' \ + CODE_SIGN_IDENTITY="-" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO + + - name: Build LogoScreenSaverApp.app (Xcode) + run: | + xcodebuild build \ + -project LogoScreenSaver.xcodeproj \ + -scheme LogoScreenSaverApp \ + -configuration Debug \ + -destination 'platform=macOS' \ + CODE_SIGN_IDENTITY="-" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..62c8c57 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,133 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +permissions: + contents: write + +jobs: + build-release: + name: Build Release Artifacts + runs-on: macos-15 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Extract Version from Tag + id: version + run: | + VERSION=${GITHUB_REF_NAME#v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Building version: $VERSION" + + - name: Generate BuildInfo.swift + run: | + mkdir -p Core/Generated + COMMIT_HASH=$(git rev-parse HEAD) + DIRTY_STATE=$(git diff --quiet && echo "false" || echo "true") + BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + cat > Core/Generated/BuildInfo.swift << EOF + // SPDX-License-Identifier: Apache-2.0 + // Auto-generated - DO NOT EDIT + + import Foundation + + public enum BuildInfo { + public static let commitHash = "$COMMIT_HASH" + public static let isDirty = $DIRTY_STATE + public static let buildTimestamp = "$BUILD_TIMESTAMP" + } + EOF + + echo "Generated BuildInfo.swift:" + cat Core/Generated/BuildInfo.swift + + - name: Build LogoScreenSaver.saver (Release) + run: | + xcodebuild build \ + -project LogoScreenSaver.xcodeproj \ + -scheme LogoScreenSaver \ + -configuration Release \ + -destination 'platform=macOS' \ + -derivedDataPath build/DerivedData \ + CODE_SIGN_IDENTITY="-" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=YES \ + MARKETING_VERSION=${{ steps.version.outputs.version }} \ + CURRENT_PROJECT_VERSION=${{ steps.version.outputs.version }} + + - name: Build LogoScreenSaverApp.app (Release) + run: | + xcodebuild build \ + -project LogoScreenSaver.xcodeproj \ + -scheme LogoScreenSaverApp \ + -configuration Release \ + -destination 'platform=macOS' \ + -derivedDataPath build/DerivedData \ + CODE_SIGN_IDENTITY="-" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=YES \ + MARKETING_VERSION=${{ steps.version.outputs.version }} \ + CURRENT_PROJECT_VERSION=${{ steps.version.outputs.version }} + + - name: Package Artifacts + run: | + mkdir -p artifacts + + # Find and package the .saver bundle + SAVER_PATH=$(find build/DerivedData -name "LogoScreenSaver.saver" -type d | head -1) + if [ -n "$SAVER_PATH" ]; then + ditto -c -k --keepParent "$SAVER_PATH" "artifacts/LogoScreenSaver-${{ steps.version.outputs.version }}.saver.zip" + echo "Packaged: LogoScreenSaver-${{ steps.version.outputs.version }}.saver.zip" + else + echo "Error: LogoScreenSaver.saver not found" + exit 1 + fi + + # Find and package the .app bundle + APP_PATH=$(find build/DerivedData -name "LogoScreenSaverApp.app" -type d | head -1) + if [ -n "$APP_PATH" ]; then + ditto -c -k --keepParent "$APP_PATH" "artifacts/LogoScreenSaverApp-${{ steps.version.outputs.version }}.app.zip" + echo "Packaged: LogoScreenSaverApp-${{ steps.version.outputs.version }}.app.zip" + else + echo "Error: LogoScreenSaverApp.app not found" + exit 1 + fi + + ls -la artifacts/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: "LogoScreenSaver ${{ steps.version.outputs.version }}" + body: | + ## LogoScreenSaver ${{ steps.version.outputs.version }} + + ### Installation + + **Screen Saver (.saver)** + 1. Download `LogoScreenSaver-${{ steps.version.outputs.version }}.saver.zip` + 2. Unzip and double-click `LogoScreenSaver.saver` + 3. Choose "Install for current user" or "Install for all users" + 4. Open System Settings > Screen Saver to configure + + **Test App (.app)** + 1. Download `LogoScreenSaverApp-${{ steps.version.outputs.version }}.app.zip` + 2. Unzip and move to Applications + 3. Right-click and select "Open" (required for ad-hoc signed apps) + + --- + + **Full Changelog**: https://github.com/${{ github.repository }}/compare/...v${{ steps.version.outputs.version }} + files: | + artifacts/LogoScreenSaver-${{ steps.version.outputs.version }}.saver.zip + artifacts/LogoScreenSaverApp-${{ steps.version.outputs.version }}.app.zip + draft: false + prerelease: ${{ contains(steps.version.outputs.version, '-') }} diff --git a/.gitignore b/.gitignore index 5da420f..159f5e3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ build/ logos/ .swiftpm/ +# Generated build info +Core/Generated/ + diff --git a/.swiftlint.yml b/.swiftlint.yml index ef45544..0e941bf 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -18,6 +18,7 @@ excluded: - .build - .swiftpm - Packages + - Core/Generated # Rule configurations cyclomatic_complexity: diff --git a/Core/Logging/DebugOverlay.swift b/Core/Logging/DebugOverlay.swift index 03cff16..ccbf514 100644 --- a/Core/Logging/DebugOverlay.swift +++ b/Core/Logging/DebugOverlay.swift @@ -25,13 +25,28 @@ public struct VersionInfo { public var sdkName: String public var xcodeVer: String public var hostVer: String + public var commitHash: String + public var isDirty: Bool + public var buildTimestamp: String - public init(appName: String, version: String, sdkName: String, xcodeVer: String, hostVer: String) { + public init( + appName: String, + version: String, + sdkName: String, + xcodeVer: String, + hostVer: String, + commitHash: String = "unknown", + isDirty: Bool = false, + buildTimestamp: String = "" + ) { self.appName = appName self.version = version self.sdkName = sdkName self.xcodeVer = xcodeVer self.hostVer = hostVer + self.commitHash = commitHash + self.isDirty = isDirty + self.buildTimestamp = buildTimestamp } public static func fromBundle(_ bundle: Bundle, appNameFallback: String = "App") -> VersionInfo { @@ -42,7 +57,10 @@ public struct VersionInfo { version: dict["CFBundleShortVersionString"] as? String ?? "Unknown", sdkName: dict["DTSDKName"] as? String ?? "Unknown", xcodeVer: dict["DTXcodeBuild"] as? String ?? "Unknown", - hostVer: dict["BuildMachineOSBuild"] as? String ?? "Unknown" + hostVer: dict["BuildMachineOSBuild"] as? String ?? "Unknown", + commitHash: BuildInfo.commitHash, + isDirty: BuildInfo.isDirty, + buildTimestamp: BuildInfo.buildTimestamp ) } } @@ -162,7 +180,15 @@ public final class DebugOverlayRenderer { state: DebugOverlayState, frame: NSRect ) -> ContentInfo { - let versionLine = "\(state.version.appName) - Version \(state.version.version), SDK \(state.version.sdkName), XCode \(state.version.xcodeVer), OS \(state.version.hostVer)" + var versionLine = "\(state.version.appName) - Version \(state.version.version)" + if state.version.commitHash != "unknown" && !state.version.commitHash.isEmpty { + let shortHash = String(state.version.commitHash.prefix(7)) + versionLine += " (\(shortHash))" + if state.version.isDirty { + versionLine += " [dirty]" + } + } + versionLine += ", SDK \(state.version.sdkName), XCode \(state.version.xcodeVer), OS \(state.version.hostVer)" let versionAttr = NSAttributedString(string: versionLine, attributes: textAttrs) let copyrightText = """ diff --git a/LogoScreenSaver.xcodeproj/project.pbxproj b/LogoScreenSaver.xcodeproj/project.pbxproj index 65dd05d..3548bf1 100644 --- a/LogoScreenSaver.xcodeproj/project.pbxproj +++ b/LogoScreenSaver.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + F2BLD0022EE3000100000BD2 /* Generated/BuildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2BLD0012EE3000000000BD1 /* Generated/BuildInfo.swift */; }; + F2BLD0032EE3000100000BD3 /* Generated/BuildInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2BLD0012EE3000000000BD1 /* Generated/BuildInfo.swift */; }; F2C001032EE3000100000002 /* Logging/DebugOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C001022EE3000000000002 /* Logging/DebugOverlay.swift */; }; F2C001052EE3000100000003 /* Animation/Logo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C001042EE3000000000003 /* Animation/Logo.swift */; }; F2C001072EE3000100000004 /* Animation/Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C001062EE3000000000004 /* Animation/Animator.swift */; }; @@ -68,6 +70,7 @@ /* Begin PBXFileReference section */ F22C56F02B258E2F00B5A43A /* LogoScreenSaver.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LogoScreenSaver.saver; sourceTree = BUILT_PRODUCTS_DIR; }; + F2BLD0012EE3000000000BD1 /* Generated/BuildInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Generated/BuildInfo.swift; sourceTree = ""; }; F2C001022EE3000000000002 /* Logging/DebugOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging/DebugOverlay.swift; sourceTree = ""; }; F2C001042EE3000000000003 /* Animation/Logo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animation/Logo.swift; sourceTree = ""; }; F2C001062EE3000000000004 /* Animation/Animator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animation/Animator.swift; sourceTree = ""; }; @@ -154,6 +157,7 @@ F2C0012C2EE3000000000027 /* Core */ = { isa = PBXGroup; children = ( + F2BLD0012EE3000000000BD1 /* Generated/BuildInfo.swift */, F2C001022EE3000000000002 /* Logging/DebugOverlay.swift */, F2C001042EE3000000000003 /* Animation/Logo.swift */, F2C001062EE3000000000004 /* Animation/Animator.swift */, @@ -334,6 +338,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F2BLD0022EE3000100000BD2 /* Generated/BuildInfo.swift in Sources */, F2C001032EE3000100000002 /* Logging/DebugOverlay.swift in Sources */, F2CFG0152EE3000100000C15 /* Configuration.swift in Sources */, F2CFG0C12EE3000100000CC1 /* Loaders/ColorsPlist.swift in Sources */, @@ -360,6 +365,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F2BLD0032EE3000100000BD3 /* Generated/BuildInfo.swift in Sources */, F2C00C242EE3000100000C24 /* ControlsKeyRenderer.swift in Sources */, F2C001132EE3000100000011 /* Logging/DebugOverlay.swift in Sources */, F2CFG0252EE3000100000C25 /* Configuration.swift in Sources */, diff --git a/Package.swift b/Package.swift index 24fe43d..01ca108 100644 --- a/Package.swift +++ b/Package.swift @@ -39,7 +39,12 @@ let package = Package( name: "LogoScreenSaver", dependencies: ["LogoScreenSaverCore"], path: "ScreenSaver", - exclude: ["Info.plist"] + exclude: [ + "Info.plist", + "thumbnail.png", + "thumbnail@2x.png", + "AppIcon.icns" + ] ), // App executable target (depends on Core and ScreenSaver for ConfigureSheetController) @@ -52,7 +57,8 @@ let package = Package( path: "App", exclude: [ "Info.plist", - "LogoScreenSaverApp.entitlements" + "LogoScreenSaverApp.entitlements", + "AppIcon.icns" ] ), diff --git a/Scripts/generate-build-info.sh b/Scripts/generate-build-info.sh new file mode 100755 index 0000000..6e86281 --- /dev/null +++ b/Scripts/generate-build-info.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# SPDX-License-Identifier: Apache-2.0 +# Generate BuildInfo.swift for local development + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="${PROJECT_DIR:-$SCRIPT_DIR/..}" +OUTPUT_DIR="$PROJECT_DIR/Core/Generated" +OUTPUT_FILE="$OUTPUT_DIR/BuildInfo.swift" + +mkdir -p "$OUTPUT_DIR" + +COMMIT_HASH=$(git -C "$PROJECT_DIR" rev-parse HEAD 2>/dev/null || echo "unknown") +DIRTY_STATE=$(git -C "$PROJECT_DIR" diff --quiet 2>/dev/null && echo "false" || echo "true") +BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +cat > "$OUTPUT_FILE" << EOF +// SPDX-License-Identifier: Apache-2.0 +// Auto-generated - DO NOT EDIT + +import Foundation + +public enum BuildInfo { + public static let commitHash = "$COMMIT_HASH" + public static let isDirty = $DIRTY_STATE + public static let buildTimestamp = "$BUILD_TIMESTAMP" +} +EOF + +echo "Generated $OUTPUT_FILE (commit: ${COMMIT_HASH:0:7}, dirty: $DIRTY_STATE)"