diff --git a/.github/workflows/swift-tests.yml b/.github/workflows/swift-tests.yml index 81856b24..5750c30d 100644 --- a/.github/workflows/swift-tests.yml +++ b/.github/workflows/swift-tests.yml @@ -142,6 +142,9 @@ jobs: - name: Checkout code uses: actions/checkout@v5 + - name: Check clean recipe safety + run: bash scripts/check-just-clean-safety.sh + - name: Build iOS (simulator, no signing) # Build both simulator architectures so CI validates every vendored # Arti simulator slice and the configuration that ships. diff --git a/Configs/Local.xcconfig.example b/Configs/Local.xcconfig.example index 899c5a62..a934cb56 100644 --- a/Configs/Local.xcconfig.example +++ b/Configs/Local.xcconfig.example @@ -3,3 +3,6 @@ DEVELOPMENT_TEAM = ABC123 // Unique bundle id to be able to register and run locally PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.$(DEVELOPMENT_TEAM) + +// App and share extension must use an App Group registered to your team. +APP_GROUP_ID = group.chat.bitchat.$(DEVELOPMENT_TEAM) diff --git a/Justfile b/Justfile index 88cb3895..9b1d3a31 100644 --- a/Justfile +++ b/Justfile @@ -1,107 +1,66 @@ -# BitChat macOS Build Justfile -# Handles temporary modifications needed to build and run on macOS +# BitChat developer commands +# +# Builds use a repository-local, ignored DerivedData directory. No recipe +# patches, restores, or removes tracked project/configuration files. + +project := "bitchat.xcodeproj" +macos_scheme := "bitchat (macOS)" +ios_scheme := "bitchat (iOS)" +derived_data := ".DerivedData" -# Default recipe - shows available commands default: - @echo "BitChat macOS Build Commands:" - @echo " just run - Build and run the macOS app" - @echo " just build - Build the macOS app only" - @echo " just clean - Clean build artifacts and restore original files" - @echo " just check - Check prerequisites" - @echo "" - @echo "Original files are preserved - modifications are temporary for builds only" + @echo "BitChat developer commands:" + @echo " just run Build and run the macOS app" + @echo " just build Build the macOS app without signing" + @echo " just test Run the SwiftPM test suite" + @echo " just test-ios Run tests on the iPhone 17 simulator" + @echo " just clean Remove repo-local build artifacts only" + @echo " just nuke Also remove nested package build caches" + @echo " just check Validate the development environment" -# Check prerequisites -check: +# Static guard against reintroducing source-restoring or source-deleting clean +# behavior. CI runs the same script directly. +check-clean-safety: + @bash scripts/check-just-clean-safety.sh + +check: check-clean-safety @echo "Checking prerequisites..." - @command -v xcodebuild >/dev/null 2>&1 || (echo "❌ xcodebuild not found. Install Xcode from App Store" && exit 1) - @xcode-select -p | grep -q "Xcode.app" || (echo "❌ Full Xcode required, not just command line tools. Install from App Store and run:\n sudo xcode-select -s /Applications/Xcode.app/Contents/Developer" && exit 1) - @test -d "/Applications/Xcode.app" || (echo "❌ Xcode.app not found in Applications folder. Install from App Store" && exit 1) - @xcodebuild -version >/dev/null 2>&1 || (echo "❌ Xcode not properly configured. Try:\n sudo xcode-select -s /Applications/Xcode.app/Contents/Developer" && exit 1) - @security find-identity -v -p codesigning | grep -q "Apple Development\|Developer ID" || (echo "⚠️ No Developer ID found - code signing may fail" && exit 0) - @echo "✅ All prerequisites met" + @command -v xcodebuild >/dev/null 2>&1 || (echo "❌ xcodebuild not found. Install full Xcode." && exit 1) + @developer_dir="$$(xcode-select -p 2>/dev/null)"; case "$$developer_dir" in *.app/Contents/Developer) ;; *) echo "❌ Full Xcode is not selected. Run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer"; exit 1;; esac + @xcodebuild -version + @echo "✅ Development environment ready (a signing identity is not required for just build)" -# Backup original files -backup: - @echo "Backing up original project configuration..." - @if [ -f bitchat.xcodeproj/project.pbxproj ]; then cp bitchat.xcodeproj/project.pbxproj bitchat.xcodeproj/project.pbxproj.backup; fi - @if [ -f bitchat/Info.plist ]; then cp bitchat/Info.plist bitchat/Info.plist.backup; fi - -# Restore original files -restore: - @echo "Restoring original project configuration..." - @if [ -f project.yml.backup ]; then mv project.yml.backup project.yml; fi - @# Restore iOS-specific files - @if [ -f bitchat/LaunchScreen.storyboard.ios ]; then mv bitchat/LaunchScreen.storyboard.ios bitchat/LaunchScreen.storyboard; fi - @# Use git to restore all modified files except Justfile - @git checkout -- project.yml bitchat.xcodeproj/project.pbxproj bitchat/Info.plist 2>/dev/null || echo "⚠️ Could not restore some files with git" - @# Remove any backup files - @rm -f bitchat.xcodeproj/project.pbxproj.backup bitchat/Info.plist.backup 2>/dev/null || true - -# Apply macOS-specific modifications -patch-for-macos: backup - @echo "Temporarily hiding iOS-specific files for macOS build..." - @# Move iOS-specific files out of the way temporarily - @if [ -f bitchat/LaunchScreen.storyboard ]; then mv bitchat/LaunchScreen.storyboard bitchat/LaunchScreen.storyboard.ios; fi - -# Build the macOS app -build: #check generate +build: check @echo "Building BitChat for macOS..." - @xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" build + @xcodebuild -project "{{project}}" -scheme "{{macos_scheme}}" -configuration Debug -derivedDataPath "{{derived_data}}" CODE_SIGNING_ALLOWED=NO build -# Run the macOS app run: build - @echo "Launching BitChat..." - @find ~/Library/Developer/Xcode/DerivedData -name "bitchat.app" -path "*/Debug/*" -not -path "*/Index.noindex/*" | head -1 | xargs -I {} open "{}" + @app="{{derived_data}}/Build/Products/Debug/bitchat.app"; test -d "$$app" || (echo "❌ Built app not found at $$app" && exit 1); open "$$app" -# Clean build artifacts and restore original files -clean: restore - @echo "Cleaning build artifacts..." - @rm -rf ~/Library/Developer/Xcode/DerivedData/bitchat-* 2>/dev/null || true - @# Only remove the generated project if we have a backup, otherwise use git - @if [ -f bitchat.xcodeproj/project.pbxproj.backup ]; then \ - rm -rf bitchat.xcodeproj; \ - else \ - git checkout -- bitchat.xcodeproj/project.pbxproj 2>/dev/null || echo "⚠️ Could not restore project.pbxproj"; \ - fi - @rm -f project-macos.yml 2>/dev/null || true - @echo "✅ Cleaned and restored original files" +# Backward-compatible alias for the old quick-run recipe. +dev-run: run -# Quick run without cleaning (for development) -dev-run: check - @echo "Quick development build..." - @xcodebuild -project bitchat.xcodeproj -scheme "bitchat_macOS" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" build - @find ~/Library/Developer/Xcode/DerivedData -name "bitchat.app" -path "*/Debug/*" -not -path "*/Index.noindex/*" | head -1 | xargs -I {} open "{}" +test: + @swift test + +test-ios: check + @xcodebuild -project "{{project}}" -scheme "{{ios_scheme}}" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' -derivedDataPath "{{derived_data}}" test + +# Artifact-only cleanup. In particular, this recipe never invokes Git and +# never writes, moves, restores, or removes source/configuration files. +clean: + @echo "Cleaning repo-local build artifacts..." + @rm -rf -- "{{derived_data}}" ".build" + @echo "✅ Cleaned {{derived_data}} and .build; tracked files were untouched" + +# Retain the familiar command, but keep it artifact-only as well. +nuke: clean + @echo "Cleaning nested package build caches..." + @find localPackages -type d -name .build -prune -exec rm -rf -- {} + + @rm -rf -- ".cache" + @echo "✅ Removed repository build caches; tracked files were untouched" -# Show app info info: - @echo "BitChat - Decentralized Mesh Messaging" - @echo "======================================" - @echo "• Native macOS SwiftUI app" - @echo "• Bluetooth LE mesh networking" - @echo "• End-to-end encryption" - @echo "• No internet required" - @echo "• Works offline with nearby devices" - @echo "" - @echo "Requirements:" - @echo "• macOS 13.0+ (Ventura)" - @echo "• Bluetooth LE capable Mac" - @echo "• Physical device (no simulator support)" - @echo "" - @echo "Usage:" - @echo "• Set nickname and start chatting" - @echo "• Use /join #channel for group chats" - @echo "• Use /msg @user for private messages" - @echo "• Triple-tap logo for emergency wipe" - -# Force clean everything (nuclear option) -nuke: - @echo "🧨 Nuclear clean - removing all build artifacts and backups..." - @rm -rf ~/Library/Developer/Xcode/DerivedData/bitchat-* 2>/dev/null || true - @rm -rf bitchat.xcodeproj 2>/dev/null || true - @rm -f bitchat.xcodeproj/project.pbxproj.backup 2>/dev/null || true - @rm -f bitchat/Info.plist.backup 2>/dev/null || true - @# Restore iOS-specific files if they were moved - @if [ -f bitchat/LaunchScreen.storyboard.ios ]; then mv bitchat/LaunchScreen.storyboard.ios bitchat/LaunchScreen.storyboard; fi - @git checkout bitchat.xcodeproj/project.pbxproj bitchat/Info.plist 2>/dev/null || echo "⚠️ Not a git repo or no changes to restore" - @echo "✅ Nuclear clean complete" + @echo "BitChat - decentralized mesh messaging" + @echo "macOS 13+ and iOS 16+" + @echo "Bluetooth mesh behavior requires physical Bluetooth-capable devices" diff --git a/README.md b/README.md index eef7bb40..9ad608f0 100644 --- a/README.md +++ b/README.md @@ -93,30 +93,62 @@ For detailed protocol documentation, see the [Technical Whitepaper](WHITEPAPER.m ### Option 1: Using Xcode - ```bash - cd bitchat - open bitchat.xcodeproj - ``` +```bash +open bitchat.xcodeproj +``` - To run on a device there're a few steps to prepare the code: - - Clone the local configs: `cp Configs/Local.xcconfig.example Configs/Local.xcconfig` - - Add your Developer Team ID into the newly created `Configs/Local.xcconfig` - - Bundle ID would be set to `chat.bitchat.` (unless you set to something else) - - Entitlements need to be updated manually (TODO: Automate): - - Search and replace `group.chat.bitchat` with `group.` (e.g. `group.chat.bitchat.ABC123`) +For a signed device build, create your ignored local configuration and replace +the example team ID with your Apple Developer Team ID: + +```bash +cp Configs/Local.xcconfig.example Configs/Local.xcconfig +``` + +`Local.xcconfig.example` derives unique app and App Group identifiers from that +team ID. The entitlement files already reference `$(APP_GROUP_ID)`, so tracked +project or entitlement files do not need to be edited. + +Useful command-line checks from the repository root: + +```bash +# macOS Debug build without signing +xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" \ + -configuration Debug CODE_SIGNING_ALLOWED=NO build + +# Full SwiftPM test suite +swift test + +# iOS simulator tests +xcodebuild -project bitchat.xcodeproj -scheme "bitchat (iOS)" \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 17' test +``` + +If `iPhone 17` is unavailable, choose an installed simulator from: + +```bash +xcodebuild -showdestinations -project bitchat.xcodeproj -scheme "bitchat (iOS)" +``` ### Option 2: Using `just` - ```bash - brew install just - ``` +```bash +brew install just +just check +just run +``` -Want to try this on macos: `just run` will set it up and run from source. -Run `just clean` afterwards to restore things to original state for mobile app building and development. +`just build` and `just run` use the current `bitchat (macOS)` scheme and keep +Xcode output in the ignored `.DerivedData/` directory. They never patch source, +project, configuration, or entitlement files. + +`just clean` removes only `.DerivedData/` and `.build/`. It does not invoke Git +or restore tracked files, so uncommitted work is preserved. `just test` runs the +SwiftPM suite and `just test-ios` runs the iPhone 17 simulator suite. ## Localization -- Base app resources live under `bitchat/Localization/Base.lproj/`. Add new copy to `Localizable.strings` and plural rules to `Localizable.stringsdict`. -- Share extension strings are separate in `bitchatShareExtension/Localization/Base.lproj/Localizable.strings`. +- App localizations live in `bitchat/Localizable.xcstrings`. +- Share extension strings are separate in `bitchatShareExtension/Localization/Localizable.xcstrings`. - Prefer keys that describe intent (`app_info.features.offline.title`) and reuse existing ones where possible. - Run `xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" -configuration Debug CODE_SIGNING_ALLOWED=NO build` to compile-check any localization updates. diff --git a/scripts/check-just-clean-safety.sh b/scripts/check-just-clean-safety.sh new file mode 100644 index 00000000..ca241119 --- /dev/null +++ b/scripts/check-just-clean-safety.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +tracked_justfiles="$(git ls-files | awk 'tolower($0) == "justfile"')" +tracked_justfile_count="$(printf '%s\n' "$tracked_justfiles" | awk 'NF { count++ } END { print count + 0 }')" +if [[ $tracked_justfile_count -ne 1 || $tracked_justfiles != "Justfile" ]]; then + echo "Expected exactly one tracked canonical Justfile; found: ${tracked_justfiles:-none}" >&2 + exit 1 +fi + +if ! grep -Fxq 'clean:' Justfile; then + echo "Clean recipe must not depend on another recipe" >&2 + exit 1 +fi + +clean_recipe="$({ + awk ' + /^clean:/ { in_clean = 1; next } + in_clean && /^[^[:space:]]/ { exit } + in_clean { print } + ' Justfile +})" + +if [[ -z ${clean_recipe//[[:space:]]/} ]]; then + echo "Justfile clean recipe is missing or empty" >&2 + exit 1 +fi + +if ! grep -Fxq 'derived_data := ".DerivedData"' Justfile; then + echo "Derived data path must remain the ignored repo-local .DerivedData directory" >&2 + exit 1 +fi + +clean_forbidden='git[[:space:]]+(checkout|restore|reset|clean)|(^|[[:space:]])(cp|mv)([[:space:]]|$)|bitchat\.xcodeproj|project\.pbxproj|Info\.plist|LaunchScreen|project\.yml|Configs/' +if grep -Eiq "$clean_forbidden" <<<"$clean_recipe"; then + echo "Unsafe source/configuration mutation found in the clean recipe:" >&2 + grep -Ein "$clean_forbidden" <<<"$clean_recipe" >&2 + exit 1 +fi + +if ! grep -Fq 'rm -rf -- "{{derived_data}}" ".build"' <<<"$clean_recipe"; then + echo "Clean recipe must remain limited to the declared repo-local artifact paths" >&2 + exit 1 +fi + +clean_rm_count="$(grep -Ec '^[[:space:]]*@?rm[[:space:]]+-rf([[:space:]]|$)' <<<"$clean_recipe" || true)" +if [[ $clean_rm_count -ne 1 ]]; then + echo "Clean recipe must contain exactly one recursive removal command" >&2 + exit 1 +fi + +expected_clean_recipe=' @echo "Cleaning repo-local build artifacts..." + @rm -rf -- "{{derived_data}}" ".build" + @echo "✅ Cleaned {{derived_data}} and .build; tracked files were untouched"' +if [[ $clean_recipe != "$expected_clean_recipe" ]]; then + echo "Clean recipe contains commands outside the reviewed artifact-only implementation" >&2 + exit 1 +fi + +file_forbidden='git[[:space:]]+(checkout|restore|reset|clean)|rm[[:space:]]+-rf[^#]*(bitchat\.xcodeproj|bitchat/|Configs/)|LaunchScreen\.storyboard\.ios|project\.pbxproj\.backup|Info\.plist\.backup' +if grep -Ein "$file_forbidden" Justfile; then + echo "Unsafe tracked-file recovery/deletion logic found in Justfile" >&2 + exit 1 +fi + +echo "Justfile clean safety check passed"