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
26 changes: 13 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ jobs:
# recognises the dev lane by this exact string
# (FormFactor.devBundleIdentifier) and any other id would
# resolve to the release fallback.
id="$(plutil -extract CFBundleIdentifier raw dist/OnetimePad.app/Contents/Info.plist)"
id="$(plutil -extract CFBundleIdentifier raw "dist/OnetimePad Debug.app/Contents/Info.plist")"
case "$id" in
dev.onetimesecret.pad.debug) echo "bundle id: $id" ;;
*) echo "expected the dev bundle id dev.onetimesecret.pad.debug, got: $id" >&2; exit 1 ;;
Expand All @@ -130,19 +130,19 @@ jobs:
# Bundle.main under xctest is the test runner and carries none
# of these keys. The reach no Swift test has is the packaging
# script itself dropping or overriding the key on the way into
# dist/OnetimePad.app, and that is the regression this step
# the dist/ bundle, and that is the regression this step
# exists for. Both halves are wanted; neither replaces the
# other.
sudden="$(plutil -extract NSSupportsSuddenTermination raw dist/OnetimePad.app/Contents/Info.plist)"
sudden="$(plutil -extract NSSupportsSuddenTermination raw "dist/OnetimePad Debug.app/Contents/Info.plist")"
test "$sudden" = "true" \
|| { echo "expected NSSupportsSuddenTermination true, got: $sudden" >&2; exit 1; }
# The pre-signature digest is the published artifact; an
# empty file means the hash pipeline broke silently.
test -s dist/OnetimePad.presig.sha256 \
|| { echo "dist/OnetimePad.presig.sha256 is missing or empty" >&2; exit 1; }
cmp THIRD_PARTY_NOTICES.md dist/OnetimePad.app/Contents/Resources/THIRD_PARTY_NOTICES.md
test -s "dist/OnetimePad Debug.presig.sha256" \
|| { echo "dist/OnetimePad Debug.presig.sha256 is missing or empty" >&2; exit 1; }
cmp THIRD_PARTY_NOTICES.md "dist/OnetimePad Debug.app/Contents/Resources/THIRD_PARTY_NOTICES.md"
cmp THIRD_PARTY_NOTICES.md bindings/CompanionCore.xcframework/THIRD_PARTY_NOTICES.md
cat dist/OnetimePad.presig.sha256
cat "dist/OnetimePad Debug.presig.sha256"

# The release lane, which exists for the checks that only the release
# configuration reaches. package-app.sh refuses to ship a binary
Expand Down Expand Up @@ -202,23 +202,23 @@ jobs:
# cannot collide with the dev lane or an App Store build on
# defaults, keychain items, or the state directory they
# derive from it (ADR-0012, and ADR-0016 case 4).
id="$(plutil -extract CFBundleIdentifier raw dist/OnetimePad.app/Contents/Info.plist)"
id="$(plutil -extract CFBundleIdentifier raw "dist/OnetimePad Local.app/Contents/Info.plist")"
case "$id" in
dev.onetimesecret.pad) echo "bundle id: $id" ;;
*) echo "expected the local bundle id dev.onetimesecret.pad, got: $id" >&2; exit 1 ;;
esac
# ADR-0016 case 2, on the plist that actually ships. The
# debug job carries the note on why the packaged bundle, and
# not only the source plist, is worth asserting.
sudden="$(plutil -extract NSSupportsSuddenTermination raw dist/OnetimePad.app/Contents/Info.plist)"
sudden="$(plutil -extract NSSupportsSuddenTermination raw "dist/OnetimePad Local.app/Contents/Info.plist")"
test "$sudden" = "true" \
|| { echo "expected NSSupportsSuddenTermination true, got: $sudden" >&2; exit 1; }
test -s dist/OnetimePad.presig.sha256 \
|| { echo "dist/OnetimePad.presig.sha256 is missing or empty" >&2; exit 1; }
cmp THIRD_PARTY_NOTICES.md dist/OnetimePad.app/Contents/Resources/THIRD_PARTY_NOTICES.md
test -s "dist/OnetimePad Local.presig.sha256" \
|| { echo "dist/OnetimePad Local.presig.sha256 is missing or empty" >&2; exit 1; }
cmp THIRD_PARTY_NOTICES.md "dist/OnetimePad Local.app/Contents/Resources/THIRD_PARTY_NOTICES.md"
cmp THIRD_PARTY_NOTICES.md bindings/CompanionCore.xcframework/THIRD_PARTY_NOTICES.md
test "$(cat bindings/CompanionCore.features)" = "release"
cat dist/OnetimePad.presig.sha256
cat "dist/OnetimePad Local.presig.sha256"

# The ADR conventions (docs/adr/README.md) are structural enough to
# check: required metadata, the four sections every record carries,
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Development bundles no longer take the TestFlight copy's name.**
`scripts/dev.sh` builds `dist/OnetimePad Debug.app` and
`scripts/install.sh` installs `/Applications/OnetimePad Local.app`;
only `package-app.sh --app-store` still produces `OnetimePad.app`.
The names live in `scripts/build-lanes.sh` beside the bundle ids.
`install.sh` removes a local install it left at
`/Applications/OnetimePad.app` only when that bundle carries the local
id, so a TestFlight install there is untouched.

- **Settings offers two page layouts, Tabs and Timeline** (app 0.26.0).
Tabs draws named pages along the bottom at full width; Timeline, still
a prototype, groups live pages by day down the side. The separate
Expand Down
9 changes: 5 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ disabled).
## Verifying a build

The packaging script hashes the app bundle it has just assembled,
before `codesign` runs, and writes the result to
`dist/OnetimePad.presig.sha256`.
before `codesign` runs, and writes the result beside it:
`dist/OnetimePad Local.presig.sha256` for the default local build,
`dist/OnetimePad.presig.sha256` for an App Store build.

The file holds one line, `<sha256> OnetimePad.app`. The digest is
The file holds one line, `<sha256> <bundle name>.app`. The digest is
taken over every regular file in the bundle, by relative path and
content: `find` the bundle, sort the paths under `LC_ALL=C` so the
order is byte order rather than the caller's locale, hash each file,
Expand All @@ -36,7 +37,7 @@ To reproduce it:
git checkout <commit> # clean tree, see the note below
scripts/build-core.sh
scripts/package-app.sh
cat dist/OnetimePad.presig.sha256
cat "dist/OnetimePad Local.presig.sha256"
```

Compare that line against the published one. The build script stamps
Expand Down
9 changes: 6 additions & 3 deletions docs/dogfood/DOGFOOD.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ scripts/install.sh
```

Builds the core only if it is stale, builds the app, and installs it
to `/Applications` (override with `APP_DEST`). Running the installed
to `/Applications` as `OnetimePad Local.app` (override the directory
with `APP_DEST`). A TestFlight install keeps `OnetimePad.app`. A local
install left at that name by an older `scripts/install.sh` is removed,
and only when its bundle id is `dev.onetimesecret.pad`. Running the installed
copy matters for two reasons:

- Rebuilds in the repo never touch it. `swift build` re-signs whatever
Expand Down Expand Up @@ -325,8 +328,8 @@ binary from a terminal still shows those lines as they happen, and is
still the fastest loop while you are working on the core:

```bash
osascript -e 'tell application "/Applications/OnetimePad.app" to quit'
/Applications/OnetimePad.app/Contents/MacOS/OnetimePad
osascript -e 'tell application "/Applications/OnetimePad Local.app" to quit'
"/Applications/OnetimePad Local.app/Contents/MacOS/OnetimePad"
```

Reach for `log show` for the launch you cannot reproduce, and the
Expand Down
4 changes: 2 additions & 2 deletions docs/qa/verification-procedures/file-draft-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ as the state directory is.

## Case 1: a kill with a dirty file open

1. Launch `dist/OnetimePad.app`. Open `/tmp/qa-draft.txt` through File >
1. Launch `dist/OnetimePad Debug.app`. Open `/tmp/qa-draft.txt` through File >
Open. Confirm the tab appears in the FILES group, that it carries no
countdown gauge, and that the header reads saved.
2. Type a distinctive line into the file, for example
Expand All @@ -90,7 +90,7 @@ as the state directory is.
`drafts.sealed` exists and has a recent mtime.
4. Note the wall clock time. This is the draft's last edit time, and
step 6 checks it.
5. `pkill -9 -f 'dist/OnetimePad\.app'`, then `ls -la "$STATE"` and
5. `pkill -9 -f 'dist/OnetimePad Debug\.app'`, then `ls -la "$STATE"` and
record every
file present, in particular anything matching `*.[0-9a-f]*.tmp`.
6. Launch the app.
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/verification-procedures/pinned-over-fullscreen.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ next. This procedure is what tells the three outcomes apart.

Quit any running copy first (`scripts/quit-app.sh`; only the graceful
path saves state), then `scripts/package-app.sh && open
dist/OnetimePad.app`.
"dist/OnetimePad Local.app"`.

In a second terminal, watch the surface's own log, which now carries the
gate:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ not document and a unit test cannot reach, hence "verify on hardware".

Quit any running instance first (`swift build` re-signs in place and
SIGKILLs a live one), then
`scripts/package-app.sh && open dist/OnetimePad.app`.
`scripts/package-app.sh && open "dist/OnetimePad Local.app"`.

- Raise with ⌃⌥Space, press the header, drag slowly across the screen.
**Pass:** the card stays glued to the pointer for the whole sweep.
Expand Down
8 changes: 4 additions & 4 deletions docs/qa/verification-procedures/re-signed-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ in `SUPERSEDED_MAGICS`, never on a key failure
2. Record the evidence that must not change:

```sh
codesign -dv --verbose=4 /Applications/OnetimePad.app 2>&1 | grep -E 'Identifier|Authority|TeamIdentifier'
codesign -dv --verbose=4 "/Applications/OnetimePad Local.app" 2>&1 | grep -E 'Identifier|Authority|TeamIdentifier'
ls -la "$STATE"
shasum -a 256 "$STATE/state.sealed"
```
Expand All @@ -70,9 +70,9 @@ in `SUPERSEDED_MAGICS`, never on a key failure
the easiest different identity:

```sh
codesign --force --deep --sign - /Applications/OnetimePad.app
codesign --verify --strict /Applications/OnetimePad.app
codesign -dv --verbose=4 /Applications/OnetimePad.app 2>&1 | grep Authority
codesign --force --deep --sign - "/Applications/OnetimePad Local.app"
codesign --verify --strict "/Applications/OnetimePad Local.app"
codesign -dv --verbose=4 "/Applications/OnetimePad Local.app" 2>&1 | grep Authority
```

Signing with a second real certificate instead is equally valid and
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/verification-procedures/reboot.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ scripts/install.sh

`scripts/install.sh` builds the core, packages the release bundle, asks
a running installed copy to quit gracefully, and installs to
`/Applications/OnetimePad.app`. Pin `CODESIGN_IDENTITY` in the
`/Applications/OnetimePad Local.app`. Pin `CODESIGN_IDENTITY` in the
local environment file (template: `environments/example/.env.example`) first,
or every reinstall resets the Keychain
confirmations this procedure depends on.
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/verification-procedures/spaces-and-cmd-tab.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ does not.

Quit any running copy first (`scripts/quit-app.sh`; only the graceful
path saves state), then `scripts/package-app.sh && open
dist/OnetimePad.app`.
"dist/OnetimePad Local.app"`.

Have at least three desktops (Mission Control, add two), and close both
Settings and About before starting, since an open ordinary window is the
Expand Down
9 changes: 9 additions & 0 deletions scripts/build-lanes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
PRODUCTION_BUNDLE_ID="com.onetimesecret.pad"
LOCAL_BUNDLE_ID="dev.onetimesecret.pad"
DEV_BUNDLE_ID="dev.onetimesecret.pad.debug"
# Bundle file names. Only App Store builds take the plain name, so a local
# install never replaces /Applications/OnetimePad.app, the path a TestFlight
# install occupies, and a debug bundle never reads as either in Finder.
PRODUCTION_APP_NAME="OnetimePad"
LOCAL_APP_NAME="OnetimePad Local"
DEV_APP_NAME="OnetimePad Debug"
ENVIRONMENTS_DIR="${ONETIMEPAD_ENVIRONMENTS_DIR:-$HOME/.local/appledev/CompanionApp/environments}"

SIGNING_VARIABLES=(CODESIGN_IDENTITY INSTALLER_IDENTITY PROVISIONING_PROFILE)
Expand Down Expand Up @@ -89,18 +95,21 @@ select_build_lane() { # <dev|local|app-store>
dev)
CONFIG="debug"
BUILD_BUNDLE_ID="$DEV_BUNDLE_ID"
BUILD_APP_NAME="$DEV_APP_NAME"
INSTALLER_IDENTITY=""
PROFILE_CLASS="development"
;;
local)
CONFIG="release"
BUILD_BUNDLE_ID="$LOCAL_BUNDLE_ID"
BUILD_APP_NAME="$LOCAL_APP_NAME"
INSTALLER_IDENTITY=""
PROFILE_CLASS="development"
;;
app-store)
CONFIG="release"
BUILD_BUNDLE_ID="$PRODUCTION_BUNDLE_ID"
BUILD_APP_NAME="$PRODUCTION_APP_NAME"
PROFILE_CLASS="app-store"
;;
esac
Expand Down
7 changes: 5 additions & 2 deletions scripts/dev.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# The dev lane: rebuild whatever is stale, package the debug bundle,
# and launch it from dist/. The debug build takes its own bundle id
# and launch it from dist/ as "OnetimePad Debug.app". The debug build
# takes its own bundle id
# (dev.onetimesecret.pad.debug), a "Dev" display name and the black app icon
# (ADR-0012), so it runs
# beside the installed copy without contending for the menu bar,
Expand Down Expand Up @@ -35,6 +36,8 @@
set -euo pipefail
cd "$(dirname "$0")/.."

source scripts/build-lanes.sh

NO_LAUNCH=0
# The dev lane defaults to allowing screen capture so scripted runs and
# hand checks can screenshot the surface without a flag. The header
Expand Down Expand Up @@ -85,7 +88,7 @@ running_from() { # <absolute path>
pgrep -f "$(printf '%s' "$1" | sed 's/[][\.|$(){}?+*^]/\\&/g')" >/dev/null
}

DIST_APP="$PWD/dist/OnetimePad.app"
DIST_APP="$PWD/dist/$DEV_APP_NAME.app"
if running_from "$DIST_APP/Contents/MacOS"; then
echo "==> Asking the copy running from dist/ to quit"
quit_err=""
Expand Down
27 changes: 19 additions & 8 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
# The local lane: build the release bundle as dev.onetimesecret.pad, sign
# it, and install it to APP_DEST (default /Applications). The installed copy
# it, and install it to APP_DEST (default /Applications) as
# "OnetimePad Local.app", beside a TestFlight OnetimePad.app rather than
# over it. The installed copy
# runs from /Applications rather than from .build/ or dist/, so
# rebuilds in the repo never kill it. See
# environments/example/.env.example for pinning a signing identity
Expand Down Expand Up @@ -131,25 +133,34 @@ install_bundle() { # <app name>
# OnetimePad in Launchpad and Spotlight, over state the new id cannot
# read. It goes only after it has quit: quit_installed exits rather
# than return when the app will not go, and we never remove a live app.
migrate_legacy_bundle() { # <legacy app name>
local name="$1"
#
# The local lane later installed as OnetimePad.app, which is also the
# name a TestFlight install takes. That bundle goes only when it carries
# this lane's id, so a TestFlight copy at the same path is left alone.
migrate_legacy_bundle() { # <legacy app name> [required bundle id]
local name="$1" id="${2:-}"
local legacy="$APP_DEST/$name.app"
[[ -d "$legacy" ]] || return 0
if [[ -n "$id" ]] &&
[[ "$(plutil -extract CFBundleIdentifier raw "$legacy/Contents/Info.plist" 2>/dev/null)" != "$id" ]]; then
return 0
fi
quit_installed "$name"
echo "==> Removing legacy $legacy (this app is now OnetimePad.app)"
echo "==> Removing legacy $legacy (this app is now $BUILD_APP_NAME.app)"
rm -rf "$legacy"
}

quit_installed OnetimePad
install_bundle OnetimePad
quit_installed "$BUILD_APP_NAME"
install_bundle "$BUILD_APP_NAME"
migrate_legacy_bundle CompanionBackdrop
migrate_legacy_bundle "$PRODUCTION_APP_NAME" "$BUILD_BUNDLE_ID"

if [[ "$NO_LAUNCH" == 0 ]]; then
if ((ALLOW_CAPTURE)); then
echo "==> Launching installed app (screen capture allowed)"
open --env COMPANION_ALLOW_CAPTURE=1 "$APP_DEST/OnetimePad.app"
open --env COMPANION_ALLOW_CAPTURE=1 "$APP_DEST/$BUILD_APP_NAME.app"
else
echo "==> Launching installed app"
open "$APP_DEST/OnetimePad.app"
open "$APP_DEST/$BUILD_APP_NAME.app"
fi
fi
18 changes: 11 additions & 7 deletions scripts/package-app.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/bin/env bash
# Package the app as a real .app bundle: dist/OnetimePad.app.
# Package the app as a real .app bundle under dist/, named per lane
# (scripts/build-lanes.sh): "OnetimePad Debug.app" for --debug,
# "OnetimePad Local.app" for the local lane, and OnetimePad.app only for
# --app-store, so no development bundle takes the TestFlight copy's name.
# A bare `swift run` binary has no CFBundleIdentifier, so macOS cannot
# address it: TCC grants don't stick, per-app pickers cannot list it,
# and LaunchServices registers it as a nameless process. The bundle is
Expand All @@ -21,8 +24,8 @@
# COMPANION_ALLOW_CAPTURE set, which also seeds it on. `open` does not
# forward the caller's environment; pass the variable explicitly, and to
# either configuration:
# open --env COMPANION_ALLOW_CAPTURE=1 dist/OnetimePad.app
# open --env COMPANION_ALLOW_CAPTURE=1 /Applications/OnetimePad.app
# open --env COMPANION_ALLOW_CAPTURE=1 "dist/OnetimePad Debug.app"
# open --env COMPANION_ALLOW_CAPTURE=1 "/Applications/OnetimePad Local.app"
# Both entry points take --allow-capture, which is the same launch
# without the incantation: scripts/dev.sh --allow-capture and
# scripts/install.sh --allow-capture.
Expand All @@ -43,8 +46,9 @@
# the documented login keychain fallback.
#
# Before signing, the assembled bundle is hashed into
# dist/OnetimePad.presig.sha256, the reproducible pre-signature
# artifact ADR-0012 publishes.
# dist/<bundle name>.presig.sha256 (dist/OnetimePad.presig.sha256 for
# --app-store), the reproducible pre-signature artifact ADR-0012
# publishes.
set -euo pipefail
cd "$(dirname "$0")/.."

Expand Down Expand Up @@ -315,7 +319,7 @@ if [[ "$CONFIG" == "release" ]]; then
fi
fi

APP=dist/OnetimePad.app
APP="dist/$BUILD_APP_NAME.app"
echo "==> Assembling $APP (App $VERSION, FFI $FFI_VERSION, Core $CORE_VERSION, $CONFIG)"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
Expand Down Expand Up @@ -638,5 +642,5 @@ if ((APP_STORE_MODE)); then
fi
echo "Built $APP and $PKG (App Store build $APP_STORE_BUILD_NUMBER)."
else
echo "Built $APP. Launch with: open $APP"
echo "Built $APP. Launch with: open \"$APP\""
fi
10 changes: 5 additions & 5 deletions scripts/tests/test_build_lanes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
)
PREFIXES = ("DEV_", "LOCAL_", "APP_STORE_")
SELECTION = (
'printf "%s|%s|%s|%s|%s|%s\\n" "$CONFIG" "$BUILD_BUNDLE_ID" '
'"$CODESIGN_IDENTITY" "$INSTALLER_IDENTITY" '
'printf "%s|%s|%s|%s|%s|%s|%s\\n" "$CONFIG" "$BUILD_BUNDLE_ID" '
'"$BUILD_APP_NAME" "$CODESIGN_IDENTITY" "$INSTALLER_IDENTITY" '
'"$PROVISIONING_PROFILE" "$PROFILE_CLASS"'
)

Expand Down Expand Up @@ -73,9 +73,9 @@ def test_each_lane_selects_its_metadata_and_its_own_signing_values(self):
# App Store lane keeps one.
self.write_every_environment()
expected = {
"dev": "debug|dev.onetimesecret.pad.debug|dev-file||dev-file|development",
"local": "release|dev.onetimesecret.pad|local-file||local-file|development",
"app-store": "release|com.onetimesecret.pad|staging-file|staging-file|staging-file|app-store",
"dev": "debug|dev.onetimesecret.pad.debug|OnetimePad Debug|dev-file||dev-file|development",
"local": "release|dev.onetimesecret.pad|OnetimePad Local|local-file||local-file|development",
"app-store": "release|com.onetimesecret.pad|OnetimePad|staging-file|staging-file|staging-file|app-store",
}
for lane, values in expected.items():
with self.subTest(lane=lane):
Expand Down