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
64 changes: 64 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/build/
/CMakeUserPresets.json
.DS_Store
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 47 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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()
37 changes: 37 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
@@ -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 }
}
]
}
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# SimStore

SimStore is a macOS app for discovering and managing prebuilt open-source iOS Simulator apps on Apple Silicon Macs.
30 changes: 30 additions & 0 deletions cmake/Info.plist.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 SimStore contributors.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
21 changes: 21 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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 "$<TARGET_BUNDLE_DIR:SimStore>/Contents/MacOS/SimStore.d"
VERBATIM
)
28 changes: 28 additions & 0 deletions src/ContentView.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}
}
11 changes: 11 additions & 0 deletions src/SimStoreApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SwiftUI

@main
struct SimStoreApp: App {
var body: some Scene {
WindowGroup("SimStore") {
ContentView()
}
.defaultSize(width: 800, height: 540)
}
}
68 changes: 68 additions & 0 deletions tests/AppSmokeTest.swift
Original file line number Diff line number Diff line change
@@ -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])
}
}
27 changes: 27 additions & 0 deletions tests/BundleMetadataTest.cmake
Original file line number Diff line number Diff line change
@@ -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()
19 changes: 19 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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 "$<TARGET_BUNDLE_DIR:SimStore>")
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=$<TARGET_FILE:SimStore>"
"-DINFO_PLIST=$<TARGET_BUNDLE_DIR:SimStore>/Contents/Info.plist"
"-DEXPECTED_SDK=${SDK_VERSION}"
"-DEXPECTED_MIN_OS=${CMAKE_OSX_DEPLOYMENT_TARGET}"
-P "${CMAKE_CURRENT_SOURCE_DIR}/BundleMetadataTest.cmake"
)