feat: brand macoc add icon stable restart scripts and Forgejo CI
This commit is contained in:
parent
a0bf759abf
commit
7325c10469
27 changed files with 531 additions and 242 deletions
37
.forgejo/workflows/macos.yml
Normal file
37
.forgejo/workflows/macos.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: macoc macOS
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# A dedicated macOS Forgejo runner is required; register label macos:host.
|
||||
# It must have Command Line Tools. There is no Linux/macOS cross-build.
|
||||
runs-on: macos
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://code.forgejo.org/actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run tests and static checks
|
||||
run: bash scripts/verify.sh
|
||||
|
||||
- name: Build and package macOS app
|
||||
run: bash scripts/package.sh
|
||||
env:
|
||||
MACOC_CODESIGN_IDENTITY: "${{ vars.MACOC_CODESIGN_IDENTITY }}"
|
||||
|
||||
- name: Publish Forgejo release on version tags
|
||||
if: startsWith(forgejo.ref, 'refs/tags/v')
|
||||
env:
|
||||
FORGEJO_TOKEN: "${{ secrets.FORGEJO_TOKEN }}"
|
||||
FORGEJO_SERVER_URL: "${{ forgejo.server_url }}"
|
||||
FORGEJO_REPOSITORY: "${{ forgejo.repository }}"
|
||||
FORGEJO_REF_NAME: "${{ forgejo.ref_name }}"
|
||||
run: bash scripts/publish-release.sh
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@ build/
|
|||
.swiftpm/
|
||||
.task/
|
||||
.build/
|
||||
dist/
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Capswitch",
|
||||
name: "macoc",
|
||||
platforms: [.macOS(.v14)],
|
||||
products: [
|
||||
.executable(name: "Capswitch", targets: ["Capswitch"]),
|
||||
.executable(name: "macoc", targets: ["macoc"]),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
|
@ -13,7 +13,7 @@ let package = Package(
|
|||
path: "Sources/Capswitch"
|
||||
),
|
||||
.executableTarget(
|
||||
name: "Capswitch",
|
||||
name: "macoc",
|
||||
dependencies: ["CapswitchCore", "CapswitchHID"],
|
||||
path: "Sources/CapswitchApp",
|
||||
linkerSettings: [
|
||||
|
|
|
|||
170
README.md
170
README.md
|
|
@ -1,116 +1,128 @@
|
|||
# Capswitch
|
||||
# macoc
|
||||
|
||||
Native macOS menu-bar keyboard layout switcher, targeting macOS 14+.
|
||||
Native macOS menu-bar layout switcher for macOS Sonoma (14+) without
|
||||
the stock Caps Lock delay. Plain **Caps** switches to the next configured
|
||||
input source on **release**, including during fast typing. **Option+Caps**
|
||||
in either order toggles the real system Caps Lock state.
|
||||
|
||||
- Plain physical Caps: switch to the next enabled input source on **release**,
|
||||
bypassing the stock Caps delay and input-source HUD.
|
||||
- Caps + Option in either order: toggle the real system Caps Lock state.
|
||||
The icon is a SFW cartoon of someone thoroughly fed up with keyboard
|
||||
layout switching, complete with a literally hot seat.
|
||||
|
||||
## Architecture
|
||||
|
||||
The signed Capswitch executable itself uses IOKit to remap physical Caps
|
||||
(HID usage 0x39) to F19 (usage 0x6E). Its active CoreGraphics event tap
|
||||
suppresses F19 down/up events and switches through Carbon TIS on F19 release.
|
||||
The small native C bridge is compiled into the app; there is no external
|
||||
hidutil process, driver, helper, LaunchAgent or root component.
|
||||
Caps Lock is remapped to F19 via in-process Apple IOKit APIs, then suppressed
|
||||
by a CoreGraphics event tap. Layouts switch through Carbon TIS. No drivers,
|
||||
root daemons, third-party input libraries or hidutil permission required.
|
||||
|
||||
Apple API reference: https://developer.apple.com/library/archive/technotes/tn2450/
|
||||
Apple technical note: https://developer.apple.com/library/archive/technotes/tn2450/
|
||||
|
||||
## Build, install and autostart
|
||||
## Build and install
|
||||
|
||||
Install Apple's Command Line Tools and the Task runner. Full Xcode is not
|
||||
required. Quit any running Capswitch from its status-bar menu before rebuilding.
|
||||
Requires Apple's Command Line Tools (not the Xcode IDE). Task is optional.
|
||||
|
||||
For development:
|
||||
Quit any running **macoc** or legacy **Capswitch** before rebuilding.
|
||||
From this repository:
|
||||
|
||||
git pull origin main
|
||||
task test
|
||||
task bundle
|
||||
open build/Capswitch.app
|
||||
bash scripts/test.sh
|
||||
bash scripts/install.sh
|
||||
|
||||
For daily use, install without sudo into ~/Applications/Capswitch.app:
|
||||
With Task installed:
|
||||
|
||||
git pull origin main
|
||||
task test
|
||||
task install
|
||||
|
||||
The Taskfile builds, installs and opens the installed copy.
|
||||
Installation creates a signed macoc.app with the .icns icon in the stable
|
||||
user-owned location ~/Applications/macoc.app, then starts it through
|
||||
LaunchServices. No sudo is needed.
|
||||
|
||||
Alternatively, open the development build and choose **Install to
|
||||
~/Applications...** in the menu. Capswitch copies itself and restarts into the
|
||||
installed copy. If an older installed Capswitch is still running, quit it
|
||||
before reinstalling. Only bundles with the same bundle ID can be replaced.
|
||||
For development without installing: bash scripts/bundle.sh or task bundle.
|
||||
|
||||
From the installed copy, click **Launch at Login** in the menu. This uses
|
||||
Apple's SMAppService.mainApp, not an ad-hoc LaunchAgent. If the menu says
|
||||
approval required, open **General -> Login Items** in System Settings and
|
||||
approve Capswitch. Click the same menu item to disable auto-start.
|
||||
Launch at Login is disabled for development builds to avoid registering
|
||||
a temporary build/ path.
|
||||
The menu offers **Restart macoc**, **Install to ~/Applications…** and
|
||||
**Launch at Login**, registered via Apple's SMAppService.mainApp. If macOS
|
||||
requests approval, open System Settings -> General -> Login Items.
|
||||
|
||||
The menu also has **Restart Capswitch**. It restores the Caps HID mapping
|
||||
before exiting and launches a new copy of the same executable after the
|
||||
old process is gone.
|
||||
### Migrating from Capswitch
|
||||
|
||||
The app has a stable bundle identifier (dev.local.Capswitch) and is signed
|
||||
ad-hoc. macOS may require re-approval for Accessibility/Input Monitoring
|
||||
when moving from build/ to ~/Applications or replacing an ad-hoc signature.
|
||||
The name and executable are now macoc; the existing bundle identifier
|
||||
remains dev.local.Capswitch to preserve old Accessibility grants and HID
|
||||
ownership data where macOS permits. Do not run the old Capswitch and macoc
|
||||
simultaneously. If the old ~/Applications/Capswitch.app is installed, quit
|
||||
it and disable its Login Item before installing macoc. Then you may delete
|
||||
the old app bundle.
|
||||
|
||||
## Privacy permissions
|
||||
## Code signing and repeat Accessibility prompts
|
||||
|
||||
On first launch the app requests permissions under its own identity:
|
||||
- Accessibility to suppress F19.
|
||||
- Input Monitoring when macOS needs it to observe keyboard events.
|
||||
Restart now uses LaunchServices (open -n -a) instead of executing a raw
|
||||
binary under Contents/MacOS. That preserves the macOS .app attribution.
|
||||
|
||||
Grant permissions to Capswitch only. No system executable needs extra
|
||||
permissions. Apple requires the user to approve them in System Settings.
|
||||
For identical app binaries at the same path, Accessibility approval normally
|
||||
persists through restarts. But ad-hoc signing does **not** provide a stable
|
||||
designated requirement across code changes: each rebuild may require
|
||||
approval again.
|
||||
|
||||
Disable the stock macOS Caps layout toggle via
|
||||
**System Settings -> Keyboard -> Text Input -> Edit ->
|
||||
Use Caps Lock to switch to/from ABC**. Keep the key assigned to its normal
|
||||
Caps Lock role in Modifier Keys (not No Action).
|
||||
Capswitch does not secretly change these preferences.
|
||||
For stable identity across **rebuilt versions**, create one persistent
|
||||
local Code Signing certificate in Keychain Access named
|
||||
**macoc Local Signing** (Certificate Assistant -> Create a Certificate ->
|
||||
Self Signed Root -> Code Signing). Run bash scripts/setup-signing.sh or
|
||||
task signing-setup for the instructions. The build auto-detects that identity,
|
||||
or accepts MACOC_CODESIGN_IDENTITY for an existing signing certificate.
|
||||
It falls back to ad-hoc signing with an explicit warning otherwise.
|
||||
|
||||
## Menu and diagnostics
|
||||
Moving to the installed path or changing the signer can still require
|
||||
**one new approval**. Keep the signing identity and app path stable.
|
||||
|
||||
The status-bar menu shows current layout, native HID keyboard service
|
||||
mapping count, event tap status, permission status and diagnostics.
|
||||
Reference: https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements
|
||||
|
||||
F19 down/up counts; input source switches
|
||||
macoc cannot silently grant itself TCC permissions. macOS still asks the
|
||||
user to allow Accessibility (and optionally Input Monitoring) for the app,
|
||||
not for a system binary.
|
||||
|
||||
No typed characters are recorded. If F19 counts increase but switches do not,
|
||||
inspect enabled TIS input sources. If counts remain zero, inspect the
|
||||
HID mapping and event tap status. Pause/normal Quit restore the physical
|
||||
Caps key and preserve unrelated keyboard mappings.
|
||||
Disable macOS's own "Use Caps Lock to switch to/from ABC" under
|
||||
System Settings -> Keyboard -> Text Input -> Edit. Keep physical Caps
|
||||
assigned its normal modifier role rather than No Action.
|
||||
|
||||
Read-only native diagnostic mode:
|
||||
## Taskfile commands and shell equivalents
|
||||
|
||||
task doctor
|
||||
| Taskfile | No Task required |
|
||||
| --- | --- |
|
||||
| task lint | bash scripts/lint.sh |
|
||||
| task test | bash scripts/test.sh |
|
||||
| task verify | bash scripts/verify.sh |
|
||||
| task build | bash scripts/build.sh |
|
||||
| task icon | bash scripts/icon.sh |
|
||||
| task sign | bash scripts/sign.sh |
|
||||
| task bundle | bash scripts/bundle.sh |
|
||||
| task install | bash scripts/install.sh |
|
||||
| task doctor | bash scripts/doctor.sh |
|
||||
| task clean | bash scripts/clean.sh |
|
||||
| task package | bash scripts/package.sh |
|
||||
| task publish | bash scripts/publish-release.sh |
|
||||
| task signing-setup | bash scripts/setup-signing.sh |
|
||||
|
||||
This executes Capswitch's own binary, not hidutil, and does not change keys.
|
||||
## Forgejo CI and releases
|
||||
|
||||
## HID recovery and limitations
|
||||
The workflow at .forgejo/workflows/macos.yml builds and tests on pushes to
|
||||
main. A pushed tag such as v0.3.0 also creates a Forgejo release with a
|
||||
macOS .app ZIP and SHA-256 checksum, through the built-in scoped token.
|
||||
|
||||
Capswitch preserves pre-existing key mappings and records ownership of
|
||||
its added Caps-to-F19 pair per keyboard service. It removes only owned
|
||||
pairs when paused or quit. A forcibly terminated app may leave a pair
|
||||
active until next clean Capswitch launch/quit, keyboard service removal,
|
||||
or reboot. To recover, reopen Capswitch and Quit normally.
|
||||
**A real Mac Forgejo runner labelled macos is required.** The Linux server
|
||||
cannot build AppKit or IOKit. See [CI setup](docs/ci.md). Until a Mac runner
|
||||
is registered and its job succeeds, no release build has been produced.
|
||||
|
||||
If installation or restart fails to restore Caps, the app displays an
|
||||
error instead of silently quitting. If macOS loses permission, it surfaces
|
||||
that status in the menu.
|
||||
A release archive signed ad-hoc is intended for local evaluation.
|
||||
To distribute broadly without Gatekeeper warnings, use Developer ID
|
||||
signing and Apple's notarization service (not yet configured here).
|
||||
|
||||
Development tasks:
|
||||
## Diagnostics and recovery
|
||||
|
||||
task verify
|
||||
task lint
|
||||
task test
|
||||
task build
|
||||
task bundle
|
||||
task install
|
||||
task doctor
|
||||
task clean
|
||||
The status-bar menu shows layout, keyboard HID mapping count, event-tap state,
|
||||
permission grants, and F19 down/up counts (no typed characters are recorded).
|
||||
Run bash scripts/doctor.sh for read-only native HID diagnostics.
|
||||
|
||||
The Linux RSB host can run static checks but has no Apple AppKit/IOKit SDK.
|
||||
The macOS build and real keyboard behavior must be verified on macOS.
|
||||
Pause or Quit normally restores Caps and preserves unrelated remappings.
|
||||
If the process is killed during a remap, restart and Quit normally; macOS
|
||||
usually resets mappings when a keyboard service disappears or after reboot.
|
||||
|
||||
The Linux development VM verifies shell syntax and static source checks,
|
||||
not macOS compilation or live TCC behavior.
|
||||
|
|
|
|||
|
|
@ -3,19 +3,21 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Capswitch</string>
|
||||
<string>macoc</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Capswitch</string>
|
||||
<string>macoc</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>dev.local.Capswitch</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Capswitch</string>
|
||||
<string>macoc</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon.icns</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.0</string>
|
||||
<string>0.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<string>3</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>14.0</string>
|
||||
<key>LSUIElement</key>
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ enum AppInstallationError: LocalizedError {
|
|||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidSource:
|
||||
return "Capswitch must be launched from a valid .app bundle."
|
||||
return "macoc must be launched from a valid .app bundle."
|
||||
case .alreadyInstalled:
|
||||
return "This copy of Capswitch is already installed."
|
||||
return "This copy of macoc is already installed."
|
||||
case .destinationContainsAnotherApp:
|
||||
return "The destination contains another application. It will not be overwritten."
|
||||
case .installedCopyRunning:
|
||||
return "Quit the installed Capswitch first, then retry installation."
|
||||
return "Quit the installed macoc first, then retry installation."
|
||||
case .unsafeDestination:
|
||||
return "The installation destination is a symbolic link. No files were changed."
|
||||
case let .rollbackFailed(backupPath):
|
||||
|
|
@ -36,7 +36,7 @@ final class AppInstallationManager {
|
|||
var installedURL: URL {
|
||||
fileManager.homeDirectoryForCurrentUser
|
||||
.appendingPathComponent("Applications", isDirectory: true)
|
||||
.appendingPathComponent("Capswitch.app", isDirectory: true)
|
||||
.appendingPathComponent("macoc.app", isDirectory: true)
|
||||
}
|
||||
|
||||
var isRunningInstalledCopy: Bool {
|
||||
|
|
@ -86,10 +86,10 @@ final class AppInstallationManager {
|
|||
|
||||
let token = UUID().uuidString
|
||||
let staging = parent.appendingPathComponent(
|
||||
".Capswitch-stage-\(token).app", isDirectory: true
|
||||
".macoc-stage-\(token).app", isDirectory: true
|
||||
)
|
||||
let backup = parent.appendingPathComponent(
|
||||
".Capswitch-backup-\(token).app", isDirectory: true
|
||||
".macoc-backup-\(token).app", isDirectory: true
|
||||
)
|
||||
|
||||
defer {
|
||||
|
|
@ -102,7 +102,7 @@ final class AppInstallationManager {
|
|||
|
||||
guard Bundle(url: staging)?.bundleIdentifier == expectedBundleID,
|
||||
fileManager.isExecutableFile(
|
||||
atPath: staging.appendingPathComponent("Contents/MacOS/Capswitch").path
|
||||
atPath: staging.appendingPathComponent("Contents/MacOS/macoc").path
|
||||
) else {
|
||||
throw AppInstallationError.invalidSource
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ enum ApplicationLifecycleError: LocalizedError {
|
|||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .missingExecutable:
|
||||
return "Unable to locate the bundled Capswitch executable."
|
||||
return "Unable to locate the bundled macoc executable."
|
||||
case let .couldNotRestoreCaps(details):
|
||||
return "Caps Lock was not restored: \(details)"
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ final class ApplicationLifecycleController {
|
|||
}
|
||||
|
||||
func restartInstalledCopy(at applicationURL: URL) throws {
|
||||
let executableURL = applicationURL.appendingPathComponent("Contents/MacOS/Capswitch")
|
||||
let executableURL = applicationURL.appendingPathComponent("Contents/MacOS/macoc")
|
||||
try relaunch(executableURL: executableURL)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,41 +2,56 @@ import Darwin
|
|||
import Foundation
|
||||
|
||||
enum RelaunchError: LocalizedError {
|
||||
case missingExecutable
|
||||
case missingBundle
|
||||
case failedToLaunch(Int32)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .missingExecutable:
|
||||
return "The application executable was not found."
|
||||
case .missingBundle:
|
||||
return "The macoc .app bundle is not available."
|
||||
case let .failedToLaunch(code):
|
||||
return "LaunchServices could not restart macoc (open exit code \(code))."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the same bundled executable. The child waits for the old PID to
|
||||
/// disappear before starting AppKit or the HID remapper.
|
||||
/// Relaunch through LaunchServices, never by directly executing
|
||||
/// Contents/MacOS/macoc. Direct binary execution can lose macOS app/TCC
|
||||
/// identity and cause fresh Accessibility prompts.
|
||||
///
|
||||
/// /usr/bin/open only launches the signed app; it is not granted input
|
||||
/// permissions and never interacts with the HID remapping.
|
||||
enum ApplicationRelauncher {
|
||||
static func spawn(executableURL: URL) throws {
|
||||
guard FileManager.default.isExecutableFile(atPath: executableURL.path) else {
|
||||
throw RelaunchError.missingExecutable
|
||||
let appURL = executableURL.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
|
||||
guard appURL.pathExtension == "app",
|
||||
FileManager.default.fileExists(atPath: appURL.path),
|
||||
FileManager.default.isExecutableFile(atPath: executableURL.path) else {
|
||||
throw RelaunchError.missingBundle
|
||||
}
|
||||
|
||||
let child = Process()
|
||||
child.executableURL = executableURL
|
||||
child.arguments = [
|
||||
"--wait-for-pid=\(ProcessInfo.processInfo.processIdentifier)"
|
||||
let launcher = Process()
|
||||
launcher.executableURL = URL(fileURLWithPath: "/usr/bin/open")
|
||||
launcher.arguments = [
|
||||
"-n", "-a", appURL.path,
|
||||
"--args", "--wait-for-pid=\(ProcessInfo.processInfo.processIdentifier)"
|
||||
]
|
||||
child.standardOutput = FileHandle.nullDevice
|
||||
child.standardError = FileHandle.nullDevice
|
||||
try child.run()
|
||||
launcher.standardOutput = FileHandle.nullDevice
|
||||
launcher.standardError = FileHandle.nullDevice
|
||||
try launcher.run()
|
||||
launcher.waitUntilExit()
|
||||
guard launcher.terminationStatus == 0 else {
|
||||
throw RelaunchError.failedToLaunch(launcher.terminationStatus)
|
||||
}
|
||||
}
|
||||
|
||||
static func waitForPreviousProcess(_ parentPID: pid_t) -> Bool {
|
||||
guard parentPID > 1 else {
|
||||
return false
|
||||
}
|
||||
guard parentPID > 1 else { return false }
|
||||
|
||||
// Bounded wait: never start a second keyboard remapper when the
|
||||
// original application refuses to terminate.
|
||||
// New instance must not own the HID mapping until the old one exits.
|
||||
for _ in 0..<200 {
|
||||
if kill(parentPID, 0) == -1 {
|
||||
return errno == ESRCH
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ enum LoginItemError: LocalizedError {
|
|||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .installFirst:
|
||||
return "Install Capswitch to ~/Applications before enabling launch at login."
|
||||
return "Install macoc to ~/Applications before enabling launch at login."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ enum InputControllerError: LocalizedError {
|
|||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .accessibilityRequired:
|
||||
return "Accessibility permission is required. Open the Capswitch menu to grant it."
|
||||
return "Accessibility permission is required. Open the macoc menu to grant it."
|
||||
case .inputMonitoringRequired:
|
||||
return "Keyboard tap refused. Grant Input Monitoring from the Capswitch menu."
|
||||
return "Keyboard tap refused. Grant Input Monitoring from the macoc menu."
|
||||
case .eventTapUnavailable:
|
||||
return "macOS did not allow the keyboard event tap. Check Accessibility and Input Monitoring permissions."
|
||||
}
|
||||
|
|
@ -159,7 +159,7 @@ final class MacOSInputController {
|
|||
func stop() -> Bool {
|
||||
// Restore the actual Caps key before disabling the F19 interceptor.
|
||||
// If restoration fails, refuse a normal Pause/Quit so F19 cannot be
|
||||
// left unhandled by an otherwise healthy running Capswitch.
|
||||
// left unhandled by an otherwise healthy running macoc.
|
||||
do {
|
||||
try remapper.restoreIfOwned()
|
||||
} catch {
|
||||
|
|
@ -257,10 +257,9 @@ final class MacOSInputController {
|
|||
_ = AXIsProcessTrustedWithOptions(options)
|
||||
}
|
||||
|
||||
if !inputMonitoringTrusted {
|
||||
// Request Input Monitoring for our own application identity.
|
||||
_ = CGRequestListenEventAccess()
|
||||
}
|
||||
// Input Monitoring is optional for an Accessibility-authorized active
|
||||
// event tap. Do not prompt on every launch when it is not needed.
|
||||
// If creating the tap fails, the menu exposes an explicit grant path.
|
||||
}
|
||||
|
||||
func requestInputMonitoringPermission() {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
|||
keyEquivalent: ""
|
||||
)
|
||||
private let restartItem = NSMenuItem(
|
||||
title: "Restart Capswitch",
|
||||
title: "Restart macoc",
|
||||
action: #selector(restartApplication),
|
||||
keyEquivalent: ""
|
||||
)
|
||||
|
|
@ -143,7 +143,7 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
|||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let quitItem = NSMenuItem(
|
||||
title: "Quit Capswitch",
|
||||
title: "Quit macoc",
|
||||
action: #selector(quit),
|
||||
keyEquivalent: "q"
|
||||
)
|
||||
|
|
@ -165,7 +165,7 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
|||
}
|
||||
|
||||
statusItem.menu = menu
|
||||
statusItem.button?.toolTip = "Capswitch"
|
||||
statusItem.button?.toolTip = "macoc"
|
||||
}
|
||||
|
||||
private func refreshController() {
|
||||
|
|
@ -211,7 +211,7 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
|||
statusItem.button?.title = inputController.isRunning
|
||||
? inputController.currentInputSourceShortName
|
||||
: "Caps!"
|
||||
statusItem.button?.toolTip = "Capswitch - \(name)"
|
||||
statusItem.button?.toolTip = "macoc - \(name)"
|
||||
|
||||
currentLayoutItem.title = "Current layout: \(name)"
|
||||
diagnosticsItem.title = inputController.diagnosticsSummary
|
||||
|
|
@ -310,9 +310,9 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
|||
|
||||
@objc private func installApplication() {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Install Capswitch?"
|
||||
alert.messageText = "Install macoc?"
|
||||
alert.informativeText =
|
||||
"Copy this version to ~/Applications/Capswitch.app and launch it from there. " +
|
||||
"Copy this version to ~/Applications/macoc.app and launch it from there. " +
|
||||
"No administrator privileges are required."
|
||||
alert.addButton(withTitle: "Install and Launch")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
|
|
@ -334,7 +334,7 @@ final class StatusBarController: NSObject, NSMenuDelegate {
|
|||
} catch {
|
||||
showError(
|
||||
"Installed, but could not relaunch",
|
||||
message: "Capswitch was installed to ~/Applications. " +
|
||||
message: "macoc was installed to ~/Applications. " +
|
||||
error.localizedDescription
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ case .doctor:
|
|||
let remapper = HIDRemappingController()
|
||||
do {
|
||||
try remapper.inspect()
|
||||
print("Capswitch - native HID diagnostic")
|
||||
print("macoc - native HID diagnostic")
|
||||
print("Keyboard services: \(remapper.keyboardServiceCount)")
|
||||
print("Caps → F19 mappings: \(remapper.mappedServiceCount)")
|
||||
if remapper.mappedServiceCount == remapper.keyboardServiceCount {
|
||||
|
|
@ -21,23 +21,23 @@ case .doctor:
|
|||
}
|
||||
exit(0)
|
||||
} catch {
|
||||
fputs("Capswitch HID check: \(error.localizedDescription)\n", stderr)
|
||||
fputs("macoc HID check: \(error.localizedDescription)\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
case .install:
|
||||
do {
|
||||
let location = try AppInstallationManager().install()
|
||||
print("Installed Capswitch: \(location.path)")
|
||||
print("Installed macoc: \(location.path)")
|
||||
exit(0)
|
||||
} catch {
|
||||
fputs("Capswitch install: \(error.localizedDescription)\n", stderr)
|
||||
fputs("macoc install: \(error.localizedDescription)\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
case let .waitForParent(pid):
|
||||
guard ApplicationRelauncher.waitForPreviousProcess(pid) else {
|
||||
fputs("Capswitch restart aborted: previous instance did not exit.\n", stderr)
|
||||
fputs("macoc restart aborted: previous instance did not exit.\n", stderr)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
|
|
|
|||
161
Taskfile.yml
161
Taskfile.yml
|
|
@ -1,124 +1,71 @@
|
|||
version: '3'
|
||||
|
||||
vars:
|
||||
APP_NAME: Capswitch
|
||||
APP_DIR: build/Capswitch.app
|
||||
RELEASE_BINARY: .build/release/Capswitch
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: verify
|
||||
|
||||
test:
|
||||
desc: Run XCTest on macOS, or report the host limitation on Linux
|
||||
cmds:
|
||||
- |
|
||||
if command -v swift >/dev/null 2>&1 && [ "$(uname -s)" = "Darwin" ]; then
|
||||
swift test
|
||||
else
|
||||
echo "SKIP: Swift/XCTest requires macOS with Command Line Tools"
|
||||
fi
|
||||
|
||||
build:
|
||||
desc: Build the macOS executable with SwiftPM
|
||||
cmds:
|
||||
- |
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
echo "SKIP: macOS build requires Darwin with Command Line Tools"
|
||||
exit 0
|
||||
fi
|
||||
swift build -c release
|
||||
|
||||
bundle:
|
||||
desc: Assemble and ad-hoc sign the menu-bar .app
|
||||
deps: [build]
|
||||
cmds:
|
||||
- |
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
echo "SKIP: macOS app bundle requires Darwin with Command Line Tools"
|
||||
exit 0
|
||||
fi
|
||||
if pgrep -x Capswitch >/dev/null 2>&1; then
|
||||
echo "ERROR: Quit running Capswitch from its menu before rebuilding." >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "{{.APP_DIR}}"
|
||||
mkdir -p "{{.APP_DIR}}/Contents/MacOS" "{{.APP_DIR}}/Contents/Resources"
|
||||
cp "{{.RELEASE_BINARY}}" "{{.APP_DIR}}/Contents/MacOS/{{.APP_NAME}}"
|
||||
cp Resources/Info.plist "{{.APP_DIR}}/Contents/Info.plist"
|
||||
codesign --force --sign - "{{.APP_DIR}}"
|
||||
|
||||
install:
|
||||
desc: Install Capswitch into ~/Applications and open it (no sudo)
|
||||
deps: [bundle]
|
||||
cmds:
|
||||
- |
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
echo "SKIP: installation requires macOS."
|
||||
exit 0
|
||||
fi
|
||||
"{{.APP_DIR}}/Contents/MacOS/{{.APP_NAME}}" --install
|
||||
open "$HOME/Applications/{{.APP_NAME}}.app"
|
||||
- bash scripts/verify.sh
|
||||
|
||||
lint:
|
||||
desc: Run host-independent source and bundle checks
|
||||
desc: Run shell and source checks
|
||||
cmds:
|
||||
- git diff --check
|
||||
- test -f Package.swift
|
||||
- test -f Resources/Info.plist
|
||||
- test -f Sources/Capswitch/CapsGestureMachine.swift
|
||||
- test -f Sources/CapswitchApp/MacOSInputController.swift
|
||||
- grep -q "LSUIElement" Resources/Info.plist
|
||||
- grep -q "CGEventType.keyDown" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- grep -q "CGEventType.keyUp" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- grep -q "keyboardEventKeycode" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- grep -q "CapsInputRouter.f19VirtualKeyCode" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- test -f Sources/CapswitchApp/HIDRemappingController.swift
|
||||
- test -f Sources/CapswitchApp/InputSourceManager.swift
|
||||
- test -f Sources/CapswitchApp/ApplicationRelauncher.swift
|
||||
- test -f Sources/CapswitchApp/ApplicationLifecycleController.swift
|
||||
- test -f Sources/CapswitchApp/LoginItemManager.swift
|
||||
- test -f Sources/CapswitchApp/AppInstallationManager.swift
|
||||
- grep -q "SMAppService.mainApp" Sources/CapswitchApp/LoginItemManager.swift
|
||||
- test -f Tests/CapswitchCoreTests/LaunchArgumentsTests.swift
|
||||
- test -f Sources/CapswitchHID/CapswitchHID.c
|
||||
- test -f Sources/CapswitchHID/CapswitchRemapping.c
|
||||
- grep -q "IOHIDServiceClientSetProperty" Sources/CapswitchHID/CapswitchRemapping.c
|
||||
- '! grep -R -F "/usr/bin/hidutil" Sources'
|
||||
- grep -q "child.executableURL = executableURL" Sources/CapswitchApp/ApplicationRelauncher.swift
|
||||
- test -f Tests/CapswitchCoreTests/CapsInputRouterTests.swift
|
||||
- grep -q "CGEvent.tapCreate" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- grep -q "CGEvent.tapEnable" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- grep -q "CGPreflightListenEventAccess" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- grep -q "CGRequestListenEventAccess" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- grep -q "CGEvent.tapIsEnabled" Sources/CapswitchApp/MacOSInputController.swift
|
||||
- test "$(grep -c "CGEventTapCreate" Sources/CapswitchApp/MacOSInputController.swift || true)" -eq 0
|
||||
- test "$(grep -c "CGEventTapEnable" Sources/CapswitchApp/MacOSInputController.swift || true)" -eq 0
|
||||
- grep -q "TISSelectInputSource" Sources/CapswitchApp/InputSourceManager.swift
|
||||
- grep -q "switchToNextInputSource" Sources/Capswitch/CapsGestureMachine.swift
|
||||
- bash scripts/lint.sh
|
||||
|
||||
test:
|
||||
desc: Run Swift XCTest on macOS
|
||||
cmds:
|
||||
- bash scripts/test.sh
|
||||
|
||||
build:
|
||||
desc: Compile macoc with SwiftPM
|
||||
cmds:
|
||||
- bash scripts/build.sh
|
||||
|
||||
icon:
|
||||
desc: Convert the icon source into an Apple .icns file
|
||||
cmds:
|
||||
- bash scripts/icon.sh
|
||||
|
||||
sign:
|
||||
desc: Sign the existing app bundle
|
||||
cmds:
|
||||
- bash scripts/sign.sh
|
||||
|
||||
bundle:
|
||||
desc: Build a signed macoc.app
|
||||
cmds:
|
||||
- bash scripts/bundle.sh
|
||||
|
||||
install:
|
||||
desc: Build, install to ~/Applications and launch
|
||||
cmds:
|
||||
- bash scripts/install.sh
|
||||
|
||||
doctor:
|
||||
desc: Inspect keyboard services through Capswitch's own native IOKit code
|
||||
desc: Inspect keyboard services through native IOKit
|
||||
cmds:
|
||||
- |
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
echo "SKIP: native HID diagnostics require macOS."
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -x "{{.APP_DIR}}/Contents/MacOS/{{.APP_NAME}}" ]; then
|
||||
echo "Build the Capswitch app first: task bundle" >&2
|
||||
exit 1
|
||||
fi
|
||||
"{{.APP_DIR}}/Contents/MacOS/{{.APP_NAME}}" --doctor
|
||||
- bash scripts/doctor.sh
|
||||
|
||||
clean:
|
||||
desc: Remove build outputs
|
||||
desc: Remove SwiftPM and packaged outputs
|
||||
cmds:
|
||||
- rm -rf build .build
|
||||
- bash scripts/clean.sh
|
||||
|
||||
verify:
|
||||
desc: Run all checks available on the current host
|
||||
desc: Run all available checks
|
||||
cmds:
|
||||
- task: lint
|
||||
- task: test
|
||||
- bash scripts/verify.sh
|
||||
|
||||
package:
|
||||
desc: Build a release zip and SHA-256 checksum
|
||||
cmds:
|
||||
- bash scripts/package.sh
|
||||
|
||||
signing-setup:
|
||||
desc: Instructions for creating a stable local signing identity
|
||||
cmds:
|
||||
- bash scripts/setup-signing.sh
|
||||
|
||||
publish:
|
||||
desc: Publish dist/ zip to Forgejo (requires FORGEJO_TOKEN)
|
||||
cmds:
|
||||
- bash scripts/publish-release.sh
|
||||
|
|
|
|||
5
scripts/build.sh
Executable file
5
scripts/build.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
cd "$ROOT"
|
||||
swift build -c release
|
||||
25
scripts/bundle.sh
Executable file
25
scripts/bundle.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
|
||||
if pgrep -x macoc >/dev/null 2>&1 || pgrep -x Capswitch >/dev/null 2>&1; then
|
||||
echo "Quit any running macoc/Capswitch before rebuilding." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "$ROOT/scripts/build.sh"
|
||||
|
||||
bin_dir="$(cd "$ROOT" && swift build -c release --show-bin-path)"
|
||||
binary="$bin_dir/macoc"
|
||||
if [[ ! -x "$binary" ]]; then
|
||||
echo "SwiftPM did not produce the macoc executable: $binary" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$APP_PATH"
|
||||
mkdir -p "$APP_PATH/Contents/MacOS" "$APP_PATH/Contents/Resources"
|
||||
cp "$binary" "$APP_PATH/Contents/MacOS/macoc"
|
||||
cp "$ROOT/Resources/Info.plist" "$APP_PATH/Contents/Info.plist"
|
||||
bash "$ROOT/scripts/icon.sh"
|
||||
bash "$ROOT/scripts/sign.sh"
|
||||
echo "Built: $APP_PATH"
|
||||
3
scripts/clean.sh
Executable file
3
scripts/clean.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
rm -rf "$ROOT/build" "$ROOT/.build" "$ROOT/dist"
|
||||
18
scripts/common.sh
Executable file
18
scripts/common.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
APP_NAME=macoc
|
||||
BUNDLE_ID=dev.local.Capswitch
|
||||
APP_PATH="$ROOT/build/macoc.app"
|
||||
SOURCE_ICON="$ROOT/Resources/IconSource.jpg"
|
||||
DIST_PATH="$ROOT/dist"
|
||||
|
||||
if [[ "$(uname -s)" == Darwin ]]; then IS_MACOS=1; else IS_MACOS=0; fi
|
||||
|
||||
macos_only() {
|
||||
if [[ "$IS_MACOS" != 1 ]]; then
|
||||
echo "SKIP: this task requires macOS with Apple Command Line Tools."
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
8
scripts/doctor.sh
Executable file
8
scripts/doctor.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
if [[ ! -x "$APP_PATH/Contents/MacOS/macoc" ]]; then
|
||||
echo "Build the app with bash scripts/bundle.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
"$APP_PATH/Contents/MacOS/macoc" --doctor
|
||||
23
scripts/icon.sh
Executable file
23
scripts/icon.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
|
||||
if [[ ! -f "$SOURCE_ICON" ]]; then
|
||||
echo "Missing icon source: $SOURCE_ICON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
iconset="$ROOT/build/macoc.iconset"
|
||||
mkdir -p "$iconset"
|
||||
|
||||
for size in 16 32 128 256 512; do
|
||||
sips -s format png -z "$size" "$size" "$SOURCE_ICON" \
|
||||
--out "$iconset/icon_${size}x${size}.png" >/dev/null
|
||||
done
|
||||
for specification in "16 32" "32 64" "128 256" "256 512" "512 1024"; do
|
||||
read -r name pixels <<<"$specification"
|
||||
sips -s format png -z "$pixels" "$pixels" "$SOURCE_ICON" \
|
||||
--out "$iconset/icon_${name}x${name}@2x.png" >/dev/null
|
||||
done
|
||||
mkdir -p "$APP_PATH/Contents/Resources"
|
||||
iconutil -c icns "$iconset" -o "$APP_PATH/Contents/Resources/AppIcon.icns"
|
||||
6
scripts/install.sh
Executable file
6
scripts/install.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
bash "$ROOT/scripts/bundle.sh"
|
||||
"$APP_PATH/Contents/MacOS/macoc" --install
|
||||
open -a "$HOME/Applications/macoc.app"
|
||||
25
scripts/lint.sh
Executable file
25
scripts/lint.sh
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
cd "$ROOT"
|
||||
git diff --check
|
||||
git diff --cached --check
|
||||
test -f Package.swift
|
||||
test -f Resources/Info.plist
|
||||
test -s Resources/IconSource.jpg
|
||||
test -f Sources/CapswitchApp/MacOSInputController.swift
|
||||
test -f Sources/CapswitchApp/InputSourceManager.swift
|
||||
test -f Sources/CapswitchApp/ApplicationRelauncher.swift
|
||||
test -f Sources/CapswitchApp/AppInstallationManager.swift
|
||||
test -f Sources/CapswitchApp/LoginItemManager.swift
|
||||
test -f Sources/CapswitchHID/CapswitchRemapping.c
|
||||
grep -q 'CFBundleIconFile' Resources/Info.plist
|
||||
grep -q 'dev.local.Capswitch' Resources/Info.plist
|
||||
grep -q 'TISSelectInputSource' Sources/CapswitchApp/InputSourceManager.swift
|
||||
grep -q 'IOHIDServiceClientSetProperty' Sources/CapswitchHID/CapswitchRemapping.c
|
||||
grep -q '/usr/bin/open' Sources/CapswitchApp/ApplicationRelauncher.swift
|
||||
grep -q 'SMAppService.mainApp' Sources/CapswitchApp/LoginItemManager.swift
|
||||
test -f .forgejo/workflows/macos.yml
|
||||
for name in build bundle test install doctor clean lint verify icon sign package publish-release setup-signing; do
|
||||
bash -n "scripts/$name.sh"
|
||||
done
|
||||
echo "lint: PASS"
|
||||
20
scripts/package.sh
Executable file
20
scripts/package.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
|
||||
version="$(/usr/libexec/PlistBuddy -c 'Print CFBundleShortVersionString' "$ROOT/Resources/Info.plist")"
|
||||
tag="${FORGEJO_REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||
if [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] && [[ "$tag" != "v$version" ]]; then
|
||||
echo "Tag $tag does not match Info.plist version v$version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bash "$ROOT/scripts/bundle.sh"
|
||||
mkdir -p "$DIST_PATH"
|
||||
name="macoc-v${version}-macos-$(uname -m).zip"
|
||||
ditto -c -k --keepParent "$APP_PATH" "$DIST_PATH/$name"
|
||||
(
|
||||
cd "$DIST_PATH"
|
||||
shasum -a 256 "$name" > "$name.sha256"
|
||||
)
|
||||
echo "Release archive: $DIST_PATH/$name"
|
||||
74
scripts/publish-release.sh
Executable file
74
scripts/publish-release.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
|
||||
: "${FORGEJO_TOKEN:?Forgejo release requires FORGEJO_TOKEN}"
|
||||
: "${FORGEJO_SERVER_URL:?Forgejo release requires FORGEJO_SERVER_URL}"
|
||||
: "${FORGEJO_REPOSITORY:?Forgejo release requires FORGEJO_REPOSITORY}"
|
||||
|
||||
tag="${FORGEJO_REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Expected a version tag like v0.3.0, got: $tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$FORGEJO_REPOSITORY" != "catsona/macoc" ]]; then
|
||||
echo "Unexpected repository: $FORGEJO_REPOSITORY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_api="$FORGEJO_SERVER_URL/api/v1/repos/$FORGEJO_REPOSITORY/releases"
|
||||
json_file="$(mktemp)"
|
||||
auth_file="$(mktemp)"
|
||||
chmod 600 "$auth_file"
|
||||
# Avoid placing the short-lived repository token in curl's argv/process list.
|
||||
printf 'header = "Authorization: token %s"\n' "$FORGEJO_TOKEN" > "$auth_file"
|
||||
trap 'rm -f "$json_file" "$auth_file"' EXIT
|
||||
|
||||
read_release() {
|
||||
curl -K "$auth_file" -fsS "$release_api/tags/$tag"
|
||||
}
|
||||
|
||||
if ! read_release > "$json_file" 2>/dev/null; then
|
||||
payload="$(python3 -c \
|
||||
'import json,sys; print(json.dumps({"tag_name":sys.argv[1], "name":sys.argv[1], "body":"macoc release for macOS. Unsigned / ad-hoc builds may require additional Gatekeeper approval. See README for signing and notarization.", "draft":False, "prerelease":False}))' \
|
||||
"$tag")"
|
||||
curl -K "$auth_file" -fsS -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" "$release_api" > "$json_file"
|
||||
fi
|
||||
|
||||
release_id="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' < "$json_file")"
|
||||
existing_assets="$(curl -K "$auth_file" -fsS \
|
||||
"$release_api/$release_id/assets")"
|
||||
|
||||
shopt -s nullglob
|
||||
archives=("$DIST_PATH/macoc-$tag-macos-"*.zip)
|
||||
if [[ "${#archives[@]}" -ne 1 ]]; then
|
||||
echo "Expected exactly one archive for $tag in $DIST_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
assets=("${archives[0]}" "${archives[0]}.sha256")
|
||||
if [[ ! -s "${assets[0]}" || ! -s "${assets[1]}" ]]; then
|
||||
echo "Missing archive/checksum pair for $tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for asset in "${assets[@]}"; do
|
||||
name="$(basename "$asset")"
|
||||
if printf '%s' "$existing_assets" | python3 -c \
|
||||
'import json,sys; name=sys.argv[1]; exit(0 if any(a.get("name")==name for a in json.load(sys.stdin)) else 1)' \
|
||||
"$name"; then
|
||||
echo "Already uploaded: $name"
|
||||
continue
|
||||
fi
|
||||
|
||||
safe_name="$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "$name")"
|
||||
curl -K "$auth_file" -fsS -X POST \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$asset" \
|
||||
"$release_api/$release_id/assets?name=$safe_name" >/dev/null
|
||||
echo "Uploaded: $name"
|
||||
done
|
||||
|
||||
echo "Published: $FORGEJO_SERVER_URL/$FORGEJO_REPOSITORY/releases/tag/$tag"
|
||||
28
scripts/setup-signing.sh
Executable file
28
scripts/setup-signing.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
|
||||
if security find-identity -v -p codesigning 2>/dev/null |
|
||||
grep -F '"macoc Local Signing"' >/dev/null; then
|
||||
echo "macoc Local Signing is already available in your Keychain."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat <<'INSTRUCTIONS'
|
||||
One-time local signing identity setup (preserves Accessibility approval
|
||||
across rebuilt versions, not needed for a plain app restart):
|
||||
|
||||
1. Open Keychain Access -> Certificate Assistant -> Create a Certificate.
|
||||
2. Name: macoc Local Signing
|
||||
3. Identity type: Self Signed Root; Certificate type: Code Signing.
|
||||
4. Create it in your login keychain; permit codesign access when prompted.
|
||||
5. Verify: security find-identity -v -p codesigning
|
||||
|
||||
The normal build automatically uses "macoc Local Signing" if available.
|
||||
You can also set MACOC_CODESIGN_IDENTITY to a valid existing identity.
|
||||
No private signing keys are generated/stored in this repository.
|
||||
|
||||
Apple:
|
||||
https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html
|
||||
INSTRUCTIONS
|
||||
open -a "Keychain Access" || true
|
||||
32
scripts/sign.sh
Executable file
32
scripts/sign.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
|
||||
if [[ ! -d "$APP_PATH" ]]; then
|
||||
echo "Bundle missing: $APP_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
identity="${MACOC_CODESIGN_IDENTITY:-}"
|
||||
if [[ -z "$identity" ]]; then
|
||||
if security find-identity -v -p codesigning 2>/dev/null |
|
||||
grep -F '"macoc Local Signing"' >/dev/null; then
|
||||
identity="macoc Local Signing"
|
||||
else
|
||||
identity="-"
|
||||
echo "WARNING: ad-hoc signing may require Accessibility reapproval after rebuilding." >&2
|
||||
echo "Run bash scripts/setup-signing.sh for a persistent local identity." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Signing identity: $identity"
|
||||
if [[ "$identity" == Developer\ ID\ Application:* ]]; then
|
||||
# Developer ID builds require Hardened Runtime and a secure timestamp.
|
||||
# Notarization still requires a separate submission with Apple credentials.
|
||||
codesign --force --sign "$identity" --identifier "$BUNDLE_ID" \
|
||||
--options runtime --timestamp "$APP_PATH"
|
||||
else
|
||||
codesign --force --sign "$identity" --identifier "$BUNDLE_ID" \
|
||||
--timestamp=none "$APP_PATH"
|
||||
fi
|
||||
codesign --verify --deep --strict "$APP_PATH"
|
||||
codesign --display --requirements - "$APP_PATH" 2>&1 | head -15
|
||||
5
scripts/test.sh
Executable file
5
scripts/test.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
macos_only
|
||||
cd "$ROOT"
|
||||
swift test
|
||||
4
scripts/verify.sh
Executable file
4
scripts/verify.sh
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/common.sh"
|
||||
bash "$ROOT/scripts/lint.sh"
|
||||
bash "$ROOT/scripts/test.sh"
|
||||
Loading…
Reference in a new issue