From eebedc7f812734aa9c32853141bc84a44aa027da Mon Sep 17 00:00:00 2001 From: Qijia Liu Date: Tue, 3 Nov 2026 13:07:35 -0500 Subject: [PATCH 1/2] Add initial macOS SwiftUI client --- .gitignore | 3 ++ AGENTS.md | 10 +++++ CMakeLists.txt | 47 +++++++++++++++++++++++ CMakePresets.json | 37 ++++++++++++++++++ README.md | 3 ++ cmake/Info.plist.in | 30 +++++++++++++++ src/CMakeLists.txt | 21 +++++++++++ src/ContentView.swift | 28 ++++++++++++++ src/SimStoreApp.swift | 11 ++++++ tests/AppSmokeTest.swift | 68 ++++++++++++++++++++++++++++++++++ tests/BundleMetadataTest.cmake | 27 ++++++++++++++ tests/CMakeLists.txt | 19 ++++++++++ 12 files changed, 304 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 README.md create mode 100644 cmake/Info.plist.in create mode 100644 src/CMakeLists.txt create mode 100644 src/ContentView.swift create mode 100644 src/SimStoreApp.swift create mode 100644 tests/AppSmokeTest.swift create mode 100644 tests/BundleMetadataTest.cmake create mode 100644 tests/CMakeLists.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dd97b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/build/ +/CMakeUserPresets.json +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..90f3e09 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# Development guidelines + +- Implement one reviewable feature at a time. Complete the relevant build and verification, report the result, and wait for review before starting the next feature. +- This repository contains the macOS client. App recipes and release publishing belong in the separate simstore-builds repository. +- Use Swift and SwiftUI with native macOS APIs. Support only Apple Silicon macOS. +- Build with CMake and Ninja using the Xcode SDK and Swift toolchain. Do not use an Xcode project or xcodebuild to build the client. +- Keep generated files and build output under build/. Do not hardcode personal machine paths. +- Write documentation in English. Keep each prose paragraph on one physical line. Keep the README focused on the project's purpose. Keep temporary progress reports and conversation history out of repository documentation. +- Keep product UI focused on user tasks. Do not show mock catalog entries, simulated install results, or implementation details as real functionality. +- Run appropriate checks and an actual build when changing build logic. Verify launch and window behavior when changing app startup. Report build, launch, and functional verification separately. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9d7f931 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 4.3) + +execute_process(COMMAND uname -sm + OUTPUT_VARIABLE HOST_PLATFORM + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY +) +if(NOT HOST_PLATFORM STREQUAL "Darwin arm64") + message(FATAL_ERROR "SimStore requires Apple Silicon macOS.") +endif() + +set(CMAKE_OSX_ARCHITECTURES arm64 CACHE STRING "Target architecture") +if(NOT CMAKE_OSX_ARCHITECTURES STREQUAL "arm64") + message(FATAL_ERROR "SimStore supports only the arm64 architecture.") +endif() +set(CMAKE_OSX_DEPLOYMENT_TARGET 14.0 CACHE STRING "Minimum macOS version") +if(CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS 14.0) + message(FATAL_ERROR "SimStore requires macOS 14.0 or later.") +endif() + +if(NOT CMAKE_OSX_SYSROOT) + execute_process(COMMAND xcrun --sdk macosx --show-sdk-path + OUTPUT_VARIABLE CMAKE_OSX_SYSROOT + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY + ) +endif() +if(NOT CMAKE_Swift_COMPILER OR CMAKE_Swift_COMPILER STREQUAL "/usr/bin/swiftc") + execute_process(COMMAND xcrun --find swiftc + OUTPUT_VARIABLE SWIFTC + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY + ) + set(CMAKE_Swift_COMPILER "${SWIFTC}" CACHE FILEPATH "Swift compiler" FORCE) +endif() +set(CMAKE_Swift_COMPILER_TARGET "arm64-apple-macos${CMAKE_OSX_DEPLOYMENT_TARGET}") + +project(SimStore VERSION 0.1.0 LANGUAGES Swift) +set(CMAKE_Swift_LANGUAGE_VERSION 6) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +add_subdirectory(src) + +include(CTest) +if(BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..29a5d03 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,37 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "release", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "debug", + "inherits": "release", + "binaryDir": "${sourceDir}/build/debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { "name": "release", "configurePreset": "release" }, + { "name": "debug", "configurePreset": "debug" } + ], + "testPresets": [ + { + "name": "release", + "configurePreset": "release", + "output": { "outputOnFailure": true } + }, + { + "name": "debug", + "configurePreset": "debug", + "output": { "outputOnFailure": true } + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..aaa8886 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# SimStore + +SimStore is a macOS app for discovering and managing prebuilt open-source iOS Simulator apps on Apple Silicon Macs. diff --git a/cmake/Info.plist.in b/cmake/Info.plist.in new file mode 100644 index 0000000..6b6f017 --- /dev/null +++ b/cmake/Info.plist.in @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + Copyright © 2026 SimStore contributors. + NSPrincipalClass + NSApplication + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..297a9c4 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,21 @@ +add_executable(SimStore MACOSX_BUNDLE + SimStoreApp.swift + ContentView.swift +) +# Preserve the SDK version in LC_BUILD_VERSION when invoking swiftc directly. +target_link_options(SimStore PRIVATE + "SHELL:-Xclang-linker -isysroot -Xclang-linker \"${CMAKE_OSX_SYSROOT}\"" +) +set_target_properties(SimStore PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/cmake/Info.plist.in" + MACOSX_BUNDLE_BUNDLE_NAME SimStore + MACOSX_BUNDLE_GUI_IDENTIFIER io.simstore.SimStore + MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}" + MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}" + RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}" + Swift_MODULE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/modules" +) +add_custom_command(TARGET SimStore POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E rm -f "$/Contents/MacOS/SimStore.d" + VERBATIM +) diff --git a/src/ContentView.swift b/src/ContentView.swift new file mode 100644 index 0000000..f61a5d1 --- /dev/null +++ b/src/ContentView.swift @@ -0,0 +1,28 @@ +import AppKit +import SwiftUI + +@MainActor +struct ContentView: View { + @State private var cannotOpenRepository = false + + private let repositoryURL = URL(string: "https://github.com/eagleoflqj/simstore-builds")! + + var body: some View { + ContentUnavailableView { + Label("SimStore", systemImage: "square.stack.3d.up.fill") + } description: { + Text("Open-source apps for iOS Simulator.") + } actions: { + Button("View App Repository", systemImage: "arrow.up.right") { + cannotOpenRepository = !NSWorkspace.shared.open(repositoryURL) + } + .controlSize(.large) + } + .frame(minWidth: 640, minHeight: 420) + .alert("Couldn’t Open Repository", isPresented: $cannotOpenRepository) { + Button("OK", role: .cancel) {} + } message: { + Text("Open this address in your web browser: \(repositoryURL.absoluteString)") + } + } +} diff --git a/src/SimStoreApp.swift b/src/SimStoreApp.swift new file mode 100644 index 0000000..8d7c407 --- /dev/null +++ b/src/SimStoreApp.swift @@ -0,0 +1,11 @@ +import SwiftUI + +@main +struct SimStoreApp: App { + var body: some Scene { + WindowGroup("SimStore") { + ContentView() + } + .defaultSize(width: 800, height: 540) + } +} diff --git a/tests/AppSmokeTest.swift b/tests/AppSmokeTest.swift new file mode 100644 index 0000000..5a2cd14 --- /dev/null +++ b/tests/AppSmokeTest.swift @@ -0,0 +1,68 @@ +import CoreGraphics +import Foundation + +@main +struct AppSmokeTest { + static func main() { + do { + try verifyWindow() + } catch { + FileHandle.standardError.write(Data("App smoke test failed: \(error.localizedDescription)\n".utf8)) + exit(EXIT_FAILURE) + } + } + + static func verifyWindow() throws { + guard CommandLine.arguments.count == 2 else { + throw failure("Expected the App Bundle path.") + } + let bundleURL = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true) + guard let executable = Bundle(url: bundleURL)?.executableURL else { + throw failure("The App Bundle has no executable.") + } + let process = Process() + process.executableURL = executable + try process.run() + defer { + if process.isRunning { + process.terminate() + } + process.waitUntilExit() + } + + let deadline = Date().addingTimeInterval(10) + var foundWindow = false + while Date() < deadline { + guard process.isRunning else { + throw failure("The app exited before its window appeared (status \(process.terminationStatus)).") + } + let windows = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? [] + foundWindow = windows.contains { window in + guard (window[kCGWindowOwnerPID as String] as? Int32) == process.processIdentifier, + (window[kCGWindowLayer as String] as? Int) == 0, + let bounds = window[kCGWindowBounds as String] as? [String: Any], + let width = bounds["Width"] as? Double, + let height = bounds["Height"] as? Double else { + return false + } + return width >= 600 && height >= 400 + } + if foundWindow { + break + } + Thread.sleep(forTimeInterval: 0.1) + } + guard foundWindow else { + throw failure("No visible app window appeared. Run this test from a logged-in macOS GUI session.") + } + Thread.sleep(forTimeInterval: 2) + guard process.isRunning else { + throw failure("The app exited after showing its window.") + } + print("PASS: SimStore launched with a visible window and remained alive (PID \(process.processIdentifier)).") + } + + static func failure(_ message: String) -> NSError { + NSError(domain: "SimStoreSmokeTest", code: 1, userInfo: [NSLocalizedDescriptionKey: message]) + } +} diff --git a/tests/BundleMetadataTest.cmake b/tests/BundleMetadataTest.cmake new file mode 100644 index 0000000..f69b061 --- /dev/null +++ b/tests/BundleMetadataTest.cmake @@ -0,0 +1,27 @@ +execute_process(COMMAND xcrun vtool -show-build "${EXECUTABLE}" + OUTPUT_VARIABLE BUILD_VERSION + COMMAND_ERROR_IS_FATAL ANY +) + +function(check_build_version FIELD EXPECTED) + string(REGEX MATCH "[\r\n][ \t]+${FIELD} ([0-9.]+)" MATCH "${BUILD_VERSION}") + if(NOT MATCH) + message(FATAL_ERROR "Missing ${FIELD} in Mach-O build version:\n${BUILD_VERSION}") + endif() + set(ACTUAL "${CMAKE_MATCH_1}") + if(NOT ACTUAL VERSION_EQUAL EXPECTED) + message(FATAL_ERROR "Mach-O ${FIELD}: expected ${EXPECTED}, got ${ACTUAL}") + endif() +endfunction() + +check_build_version(sdk "${EXPECTED_SDK}") +check_build_version(minos "${EXPECTED_MIN_OS}") + +execute_process(COMMAND /usr/bin/plutil -extract LSMinimumSystemVersion raw -o - "${INFO_PLIST}" + OUTPUT_VARIABLE PLIST_MIN_OS + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY +) +if(NOT PLIST_MIN_OS VERSION_EQUAL EXPECTED_MIN_OS) + message(FATAL_ERROR "Info.plist LSMinimumSystemVersion: expected ${EXPECTED_MIN_OS}, got ${PLIST_MIN_OS}") +endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..7007897 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +add_executable(SimStoreSmokeTest AppSmokeTest.swift) +add_dependencies(SimStoreSmokeTest SimStore) +# Parse the single-file @main entry point as a library. +target_compile_options(SimStoreSmokeTest PRIVATE -parse-as-library) +add_test(NAME app-window COMMAND SimStoreSmokeTest "$") +set_tests_properties(app-window PROPERTIES TIMEOUT 20 RUN_SERIAL TRUE) + +execute_process(COMMAND xcrun --sdk "${CMAKE_OSX_SYSROOT}" --show-sdk-version + OUTPUT_VARIABLE SDK_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY +) +add_test(NAME bundle-metadata COMMAND "${CMAKE_COMMAND}" + "-DEXECUTABLE=$" + "-DINFO_PLIST=$/Contents/Info.plist" + "-DEXPECTED_SDK=${SDK_VERSION}" + "-DEXPECTED_MIN_OS=${CMAKE_OSX_DEPLOYMENT_TARGET}" + -P "${CMAKE_CURRENT_SOURCE_DIR}/BundleMetadataTest.cmake" +) From c6b5f0af0f2c1e28549d191b1cd0d6c53417741b Mon Sep 17 00:00:00 2001 From: Qijia Liu Date: Tue, 3 Nov 2026 13:19:11 -0500 Subject: [PATCH 2/2] Add CI builds and nightly release publishing --- .github/workflows/build.yml | 64 +++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..825f948 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,64 @@ +name: Build SimStore + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: build-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + runs-on: xcode-27 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Build + run: | + cmake --preset release + cmake --build --preset release + - name: Package + run: tar -cjf build/SimStore.app.tar.bz2 -C build/release SimStore.app + - name: Upload artifact + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + path: build/SimStore.app.tar.bz2 + archive: false + if-no-files-found: error + - name: Test + run: ctest --preset release --verbose + + publish: + needs: build + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + runs-on: ubuntu-26.04 + permissions: + contents: write + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: SimStore.app.tar.bz2 + path: build + - name: Update nightly release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + notes="Build from commit $GITHUB_SHA." + if gh release view latest >/dev/null 2>&1; then + gh api --method PATCH "repos/$GH_REPO/git/refs/tags/latest" \ + -f sha="$GITHUB_SHA" -F force=true + gh release upload latest build/SimStore.app.tar.bz2 --clobber + gh release edit latest --title "Nightly build" --notes "$notes" \ + --target "$GITHUB_SHA" --latest + else + gh release create latest build/SimStore.app.tar.bz2 \ + --target "$GITHUB_SHA" --title "Nightly build" --notes "$notes" --latest + fi