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
85 changes: 70 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ jobs:
ONLY_ACTIVE_ARCH=YES
ARCHS=arm64

- name: Validate release entitlements
run: |
python3 - <<'PY'
import plistlib
from pathlib import Path

base = plistlib.loads(Path("JammLab/JammLab.entitlements").read_bytes())
release = plistlib.loads(Path("JammLab/JammLab.Release.entitlements").read_bytes())
expected = dict(base)
expected["com.apple.security.network.client"] = True

if release != expected:
raise SystemExit(
"Release entitlements must equal the base entitlements plus "
"com.apple.security.network.client=true"
)
PY

build-smoke:
name: Unsigned Build Smoke (${{ matrix.configuration }})
runs-on: macos-26
Expand Down Expand Up @@ -129,21 +147,58 @@ jobs:
run: scripts/build_separator_helper.sh

- name: Build unsigned release app
run: >
xcodebuild build
-project "$XCODE_PROJECT"
-scheme "$XCODE_SCHEME"
-configuration Release
-destination "$XCODE_DESTINATION"
-derivedDataPath "$DERIVED_DATA_PATH"
CODE_SIGN_IDENTITY=-
CODE_SIGN_STYLE=Manual
DEVELOPMENT_TEAM=
PROVISIONING_PROFILE_SPECIFIER=
MARKETING_VERSION="$APP_VERSION"
CURRENT_PROJECT_VERSION="${{ github.run_number }}"
ONLY_ACTIVE_ARCH=YES
ARCHS=arm64
run: |
set -euo pipefail
update_check_build_setting=()
if [[ "$RELEASE_CHANNEL" == "stable" ]]; then
update_check_build_setting+=("SWIFT_ACTIVE_COMPILATION_CONDITIONS=JAMMLAB_STABLE_RELEASE")
fi

xcodebuild test -quiet \
-project "$XCODE_PROJECT" \
-scheme "$XCODE_SCHEME" \
-configuration Release \
-destination "$XCODE_DESTINATION" \
-derivedDataPath "$DERIVED_DATA_PATH" \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
ENABLE_TESTABILITY=YES \
ONLY_ACTIVE_ARCH=YES \
ARCHS=arm64 \
-only-testing:JammLabTests/AppVersionTests/testCompiledReleasePolicyMatchesBuildFlags \
"${update_check_build_setting[@]}"

xcodebuild build \
-project "$XCODE_PROJECT" \
-scheme "$XCODE_SCHEME" \
-configuration Release \
-destination "$XCODE_DESTINATION" \
-derivedDataPath "$DERIVED_DATA_PATH" \
CODE_SIGN_IDENTITY=- \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM= \
PROVISIONING_PROFILE_SPECIFIER= \
MARKETING_VERSION="$APP_VERSION" \
CURRENT_PROJECT_VERSION="${{ github.run_number }}" \
ONLY_ACTIVE_ARCH=YES \
ARCHS=arm64 \
"${update_check_build_setting[@]}"

app_path="$DERIVED_DATA_PATH/Build/Products/Release/JammLab.app"
built_version="$(plutil -extract CFBundleShortVersionString raw -o - "$app_path/Contents/Info.plist")"
if [[ "$built_version" != "$APP_VERSION" ]]; then
echo "Release app version '$built_version' does not match '$APP_VERSION'" >&2
exit 1
fi

entitlements_file="$(mktemp)"
trap 'rm -f "$entitlements_file"' EXIT
codesign -d --entitlements :- "$app_path" > "$entitlements_file"
network_client="$(plutil -extract 'com\.apple\.security\.network\.client' raw -o - "$entitlements_file")"
if [[ "$network_client" != "true" ]]; then
echo "Release app is missing com.apple.security.network.client=true" >&2
exit 1
fi

- name: Package unsigned DMG
run: >
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ development artifact builds use `vMAJOR.MINOR.PATCH-dev.N`.

## Unreleased

- Added a stable-release update check that shows GitHub release notes at launch, supports reminding later or skipping one version, and keeps development and beta builds offline.
- Moved audio and Stem playback preparation off the main thread, with cancellable progress, safer memory limits, and transactional project/mode switching that keeps the current audio available if preparation fails.
- Hardened the bundled Stem helper with a versioned v6 job protocol, startup capability checks, stale-helper detection, and one validated manifest for bundled models and compute modes.
- Fixed automatic Stem transcription notation to show flats, naturals, and sharps consistently with the key signature and common-practice measure rules.
Expand Down
26 changes: 24 additions & 2 deletions JammLab.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@
A14D01033000000100112233 /* NotationWindowToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14D00033000000100112233 /* NotationWindowToolbar.swift */; };
A14D01043000000100112233 /* AudioPlayerViewModel+NotationParts.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14D00043000000100112233 /* AudioPlayerViewModel+NotationParts.swift */; };
A14D01053000000100112233 /* NotationTrackInteractionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A14D00053000000100112233 /* NotationTrackInteractionModels.swift */; };
A15A01013000000100112233 /* AppUpdateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15A00013000000100112233 /* AppUpdateModels.swift */; };
A15A01023000000100112233 /* GitHubLatestReleaseClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15A00023000000100112233 /* GitHubLatestReleaseClient.swift */; };
A15A01033000000100112233 /* AppUpdateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15A00033000000100112233 /* AppUpdateCoordinator.swift */; };
A15A01043000000100112233 /* UpdateAvailableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15A00043000000100112233 /* UpdateAvailableView.swift */; };
A15A01053000000100112233 /* AppUpdateCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15A00053000000100112233 /* AppUpdateCheckerTests.swift */; };
9F8E01032C40000100112233 /* StemModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8D00012C30000100112233 /* StemModels.swift */; };
9F8E01042C40000100112233 /* StemSeparationJobModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8E00012C40000100112233 /* StemSeparationJobModels.swift */; };
9F8F01012C50000100112233 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8F00012C50000100112233 /* AppDefaults.swift */; };
Expand Down Expand Up @@ -417,6 +422,12 @@
A14D00033000000100112233 /* NotationWindowToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotationWindowToolbar.swift; sourceTree = "<group>"; };
A14D00043000000100112233 /* AudioPlayerViewModel+NotationParts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AudioPlayerViewModel+NotationParts.swift"; sourceTree = "<group>"; };
A14D00053000000100112233 /* NotationTrackInteractionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotationTrackInteractionModels.swift; sourceTree = "<group>"; };
A15A00013000000100112233 /* AppUpdateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateModels.swift; sourceTree = "<group>"; };
A15A00023000000100112233 /* GitHubLatestReleaseClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubLatestReleaseClient.swift; sourceTree = "<group>"; };
A15A00033000000100112233 /* AppUpdateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateCoordinator.swift; sourceTree = "<group>"; };
A15A00043000000100112233 /* UpdateAvailableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAvailableView.swift; sourceTree = "<group>"; };
A15A00053000000100112233 /* AppUpdateCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateCheckerTests.swift; sourceTree = "<group>"; };
A15A00063000000100112233 /* JammLab.Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = JammLab.Release.entitlements; sourceTree = "<group>"; };
9F8F00012C50000100112233 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = "<group>"; };
9F8F00022C50000100112233 /* AudioPlaybackControlling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackControlling.swift; sourceTree = "<group>"; };
A14B00013000000100112233 /* AudioPlaybackPreparation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackPreparation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -661,6 +672,7 @@
9F7003002F80000100112233 /* Transcription */,
9F8A000B2C00000100112233 /* Assets.xcassets */,
9F8A000C2C00000100112233 /* JammLab.entitlements */,
A15A00063000000100112233 /* JammLab.Release.entitlements */,
A14A00013000000100112233 /* AudioRenderAtomics.c */,
A14A00023000000100112233 /* AudioRenderAtomics.h */,
9F7000202F80000100112233 /* JammLab-Bridging-Header.h */,
Expand All @@ -674,6 +686,7 @@
9F8A00062C00000100112233 /* AnalysisResult.swift */,
9F8F00012C50000100112233 /* AppDefaults.swift */,
9F9400012CA0000100112233 /* AppSettingsStore.swift */,
A15A00013000000100112233 /* AppUpdateModels.swift */,
9F8A00162C00000100112233 /* BeatGridSettings.swift */,
9F8A000A2C00000100112233 /* ImportedAudioFile.swift */,
9F8A00072C00000100112233 /* LoopRegion.swift */,
Expand Down Expand Up @@ -706,6 +719,7 @@
9F8F00022C50000100112233 /* AudioPlaybackControlling.swift */,
A14B00013000000100112233 /* AudioPlaybackPreparation.swift */,
9FCB00012D80000100112233 /* AudioSampleConverter.swift */,
A15A00023000000100112233 /* GitHubLatestReleaseClient.swift */,
9F8A00172C00000100112233 /* BeatGridCalculator.swift */,
9FBE00012F70000100112233 /* NotationBeamLayout.swift */,
9FBE00032F70000100112233 /* NotationBeamGrouping.swift */,
Expand Down Expand Up @@ -770,6 +784,7 @@
9F8A03052C00000100112233 /* ViewModels */ = {
isa = PBXGroup;
children = (
A15A00033000000100112233 /* AppUpdateCoordinator.swift */,
9F8A00032C00000100112233 /* AudioPlayerViewModel.swift */,
9FB201012D30000100112233 /* AudioPlayerViewModel+Project.swift */,
9FDE00042F60000100112233 /* AudioPlayerViewModel+Analysis.swift */,
Expand Down Expand Up @@ -882,6 +897,7 @@
9F8B03012C10000100112233 /* JammLabTests */ = {
isa = PBXGroup;
children = (
A15A00053000000100112233 /* AppUpdateCheckerTests.swift */,
9F7000222F80000100112233 /* StemTranscriptionTests.swift */,
9F7000232F80000100112233 /* BasicPitchNativeTests.mm */,
9FAB00012CE0000100112233 /* AudioFileImporterDurationTests.swift */,
Expand Down Expand Up @@ -1039,6 +1055,7 @@
9F8C00072C20000100112233 /* TransportBarView.swift */,
9FCB00082D80000100112233 /* TransportButton.swift */,
9F9000012C60000100112233 /* TransportControlsView.swift */,
A15A00043000000100112233 /* UpdateAvailableView.swift */,
9FAF00022D02000100112233 /* WindowCloseGuard.swift */,
9F8C00082C20000100112233 /* WindowTitleUpdater.swift */,
);
Expand Down Expand Up @@ -1249,6 +1266,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A15A01013000000100112233 /* AppUpdateModels.swift in Sources */,
A15A01023000000100112233 /* GitHubLatestReleaseClient.swift in Sources */,
A15A01033000000100112233 /* AppUpdateCoordinator.swift in Sources */,
A15A01043000000100112233 /* UpdateAvailableView.swift in Sources */,
9F8A01062C00000100112233 /* AnalysisResult.swift in Sources */,
9F8F01012C50000100112233 /* AppDefaults.swift in Sources */,
9F8A01052C00000100112233 /* AudioAnalyzer.swift in Sources */,
Expand Down Expand Up @@ -1398,6 +1419,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A15A01053000000100112233 /* AppUpdateCheckerTests.swift in Sources */,
9F7001222F80000100112233 /* StemTranscriptionTests.swift in Sources */,
9F7001232F80000100112233 /* BasicPitchNativeTests.mm in Sources */,
9FAB01012CE0000100112233 /* AudioFileImporterDurationTests.swift in Sources */,
Expand Down Expand Up @@ -1731,7 +1753,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
AUTOMATION_APPLE_EVENTS = NO;
CODE_SIGN_ENTITLEMENTS = JammLab/JammLab.entitlements;
CODE_SIGN_ENTITLEMENTS = JammLab/JammLab.Release.entitlements;
CODE_SIGN_STYLE = Automatic;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
Expand All @@ -1754,7 +1776,7 @@
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = NO;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = YES;
ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
Expand Down
2 changes: 2 additions & 0 deletions JammLab/DesignSystem/AppTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ enum AppTheme {
static let notationMinHeight: CGFloat = 420
static let tunerWidth: CGFloat = 360
static let tunerMinHeight: CGFloat = 460
static let updateSheetWidth: CGFloat = 640
static let updateReleaseNotesHeight: CGFloat = 240
}

enum Colors {
Expand Down
21 changes: 21 additions & 0 deletions JammLab/JammLab.Release.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?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>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.exception.mach-lookup.global-name</key>
<array>
<string>com.apple.audioanalyticsd</string>
<string>com.apple.linkd.autoShortcut</string>
</array>
<key>com.apple.security.files.user-selected.executable</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
3 changes: 3 additions & 0 deletions JammLab/JammLabApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,22 @@ struct JammLabApp: App {
@NSApplicationDelegateAdaptor(JammLabAppDelegate.self) private var appDelegate
@StateObject private var settingsStore: AppSettingsStore
@StateObject private var viewModel: AudioPlayerViewModel
@StateObject private var updateCoordinator: AppUpdateCoordinator
@StateObject private var recentProjectsStore = RecentProjectsStore.shared

init() {
let settingsStore = AppSettingsStore()
_settingsStore = StateObject(wrappedValue: settingsStore)
_viewModel = StateObject(wrappedValue: AudioPlayerViewModel(appSettingsStore: settingsStore))
_updateCoordinator = StateObject(wrappedValue: AppUpdateCoordinator.live())
}

var body: some Scene {
WindowGroup("JammLab", id: "main") {
ContentView(viewModel: viewModel)
.frame(minWidth: AppTheme.Window.minWidth, minHeight: AppTheme.Window.minHeight)
.environment(\.appColors, AppThemeColors(palette: settingsStore.colorPalette))
.appUpdateCheckHost(coordinator: updateCoordinator)
.onAppear {
appDelegate.viewModel = viewModel
}
Expand Down
87 changes: 87 additions & 0 deletions JammLab/Models/AppUpdateModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Foundation

struct AppVersion: Comparable, Hashable, Identifiable, Sendable {
let major: Int
let minor: Int
let patch: Int

var id: String { description }

var description: String {
"\(major).\(minor).\(patch)"
}

init?(stableVersionString rawValue: String) {
let trimmedValue = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
let versionValue = trimmedValue.hasPrefix("v")
? String(trimmedValue.dropFirst())
: trimmedValue
let components = versionValue.split(separator: ".", omittingEmptySubsequences: false)

guard
components.count == 3,
components.allSatisfy({ !$0.isEmpty && $0.allSatisfy(\.isNumber) }),
let major = Int(components[0]),
let minor = Int(components[1]),
let patch = Int(components[2])
else {
return nil
}

self.major = major
self.minor = minor
self.patch = patch
}

static func < (lhs: AppVersion, rhs: AppVersion) -> Bool {
(lhs.major, lhs.minor, lhs.patch) < (rhs.major, rhs.minor, rhs.patch)
}
}

struct AppRelease: Equatable, Identifiable, Sendable {
let version: AppVersion
let title: String
let notes: String
let pageURL: URL

var id: AppVersion { version }
}

struct AppBuildMetadata: Equatable, Sendable {
let version: AppVersion?
let isStableReleaseBuild: Bool

var isStableRelease: Bool {
version != nil && isStableReleaseBuild
}

init(versionString: String?, isStableReleaseBuild: Bool) {
version = versionString.flatMap(AppVersion.init(stableVersionString:))
self.isStableReleaseBuild = isStableReleaseBuild
}

init(bundle: Bundle = .main) {
self.init(
versionString: bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String,
isStableReleaseBuild: Self.compiledIsStableReleaseBuild
)
}

static var compiledIsStableReleaseBuild: Bool {
#if JAMMLAB_STABLE_RELEASE
true
#else
false
#endif
}
}

enum AppUpdateRuntimePolicy {
static func automaticChecksEnabled(for metadata: AppBuildMetadata) -> Bool {
#if DEBUG
false
#else
metadata.isStableRelease
#endif
}
}
Loading