From a46f8c3cd134536ce00115eeb278c0bd7fe6e8bf Mon Sep 17 00:00:00 2001 From: Sami Khan <98742866+samiamjidkhan@users.noreply.github.com> Date: Sat, 2 Aug 2025 07:14:27 +0500 Subject: [PATCH] app Co-authored-by: Alex Cheema --- .DS_Store | Bin 0 -> 8196 bytes .github/workflows/build-macos-app.yml | 198 +++++++ .gitignore | 1 + app/exov2/.DS_Store | Bin 0 -> 6148 bytes app/exov2/exov2.xcodeproj/project.pbxproj | 548 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 33984 bytes .../xcshareddata/xcschemes/exov2.xcscheme | 109 ++++ .../xcschemes/xcschememanagement.plist | 32 + .../Preview Assets.xcassets/Contents.json | 6 + app/exov2/exov2/ProcessManager.swift | 377 ++++++++++++ app/exov2/exov2/exov2.entitlements | 14 + app/exov2/exov2/exov2App.swift | 115 ++++ app/exov2/exov2Tests/exov2Tests.swift | 17 + app/exov2/exov2UITests/exov2UITests.swift | 43 ++ .../exov2UITestsLaunchTests.swift | 33 ++ 16 files changed, 1500 insertions(+) create mode 100644 .DS_Store create mode 100644 .github/workflows/build-macos-app.yml create mode 100644 app/exov2/.DS_Store create mode 100644 app/exov2/exov2.xcodeproj/project.pbxproj create mode 100644 app/exov2/exov2.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 app/exov2/exov2.xcodeproj/project.xcworkspace/xcuserdata/samikhan.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 app/exov2/exov2.xcodeproj/xcshareddata/xcschemes/exov2.xcscheme create mode 100644 app/exov2/exov2.xcodeproj/xcuserdata/samikhan.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 app/exov2/exov2/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 app/exov2/exov2/ProcessManager.swift create mode 100644 app/exov2/exov2/exov2.entitlements create mode 100644 app/exov2/exov2/exov2App.swift create mode 100644 app/exov2/exov2Tests/exov2Tests.swift create mode 100644 app/exov2/exov2UITests/exov2UITests.swift create mode 100644 app/exov2/exov2UITests/exov2UITestsLaunchTests.swift diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1ad06f76caa4306fe6078e5faff1c289853ed8a2 GIT binary patch literal 8196 zcmeHMTWl3Y7@lui=VNH1A}rmNCXD3#m_|;wnk>i+n{=B26VpkUz_f_`^BJ%TfEoE39Kh zj6jS)j6jS)j6jUQjer21*&^|4ocppjE@K2@1a3+M#QPyhS;$l*N2Ls39aMx@0FvSg z5GJbYqCzqm$y6jqrKBE8kgg=9E21j~q&w-8kzFd1qf$zDhUoHv=*fs~CQkrqiWNPo?fj#qt7nUq&tTL+jIRMtKgH&=(BuU+_t>u?k-rOzc=qW z#Ua+1VwlgRK2Nn}!o+ zHIpU{_3ZSyiQASoG_ARNQ^(ewyU)*w@$8*hKj<8`+_71uw{Sk- z4`vHi=Lqqu?6HIMaL-6DTsE6_;1whwNu!hVelb{^Y0^0Q{Mvc@QSe3aY?SjRm*`3$X|dXu%q+MH)M>6T8rfZXCctWRS%nco@b}_&AP- zaSEsLD4xV~cpfj{MZAu)coT2o9ejq*@ddubxA+M^;}=}S-}ncY6jhn6EK%-ImMQhh za-~^Wr?kqME9Gbzm`FS&P3^Jhr)Y`3-YG|~7A=W;w`kjLh-kSmUBpNA>^U`a7u79W zv3l+L)EI>%>B>AC&)<-;6NnyCHr(aOT%$-b7HA6-dx_K5vC9!8BdQ!)8eAabrl zE7oHJHe(C6Vmnc_8$IYnKL(J8MMNEhjRFD`QNn|Gi0FD6j}Td(!qa#L&*BVT#w&Og zuZ6UI7w=6X;`WI|#K+@_Sn4Xcj^`dGU75HO&EwW}L7+m8QY-HNo3H)-|GGRY9wbH} zM&QOo0F@nG9pb0JwcfVkuAQWOKV^}G-KdnI3l+jd=)>zc$+15Ssh*7bQjr{$l6olp X?;ir_cVPL4ZhZd7=YL4To3Qv7`S}~V literal 0 HcmV?d00001 diff --git a/.github/workflows/build-macos-app.yml b/.github/workflows/build-macos-app.yml new file mode 100644 index 00000000..b9f01998 --- /dev/null +++ b/.github/workflows/build-macos-app.yml @@ -0,0 +1,198 @@ +name: Build and Release Exo macOS App + +on: + push: + tags: + - 'v*' # Trigger only on version tags + branches: + - main # Also build on main branch for testing + - app-staging # Add app-staging for testing + pull_request: + branches: + - main # Test builds on PRs + +jobs: + build-exov2-macos: + runs-on: macos-15 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Rust (nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: nightly + components: rustfmt, clippy + default: true + + - name: Set Rust toolchain override + run: | + rustup default nightly + cd rust && rustup override set nightly + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Install Just + run: | + brew install just + + - name: Install UV + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: uv.lock + + - name: Setup Python Environment + run: | + uv python install + uv sync --locked --all-extras + + - name: Build Rust Components + env: + RUSTFLAGS: "-A unused-imports -A dead-code -A unreachable-code" + run: | + just build-all + + - name: Install Python Bindings + run: | + uv pip install dist/exo_pyo3_bindings-*.whl + + - name: Verify Python Environment + run: | + uv run python -c "import exo_pyo3_bindings; print('Python bindings installed successfully')" + uv run python -c "import master.main; print('Master module available')" + uv run python -c "import worker.main; print('Worker module available')" + + - name: Prepare Code Signing Keychain + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }} + run: | + security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain + security default-keychain -s exov2.keychain + security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain + + echo "$MACOS_CERTIFICATE" | base64 --decode > /tmp/exov2-certificate.p12 + security import /tmp/exov2-certificate.p12 -k exov2.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + rm /tmp/exov2-certificate.p12 + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain + + PROFILES_HOME="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" + mkdir -p "$PROFILES_HOME" + PROFILE_PATH="$(mktemp "$PROFILES_HOME"/EXOV2_PP.provisionprofile)" + echo "$PROVISIONING_PROFILE" | base64 --decode > "$PROFILE_PATH" + + - name: Build Exo Swift App + env: + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + cd app/exov2 + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer + + if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == refs/tags/v* ]]; then + # Release build with code signing + security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain + SIGNING_IDENTITY=$(security find-identity -v -p codesigning | awk -F '"' '{print $2}') + + xcodebuild clean build \ + -project exov2.xcodeproj \ + -scheme exov2 \ + -configuration Release \ + -derivedDataPath build \ + CODE_SIGNING_IDENTITY="$SIGNING_IDENTITY" \ + PROVISIONING_PROFILE_SPECIFIER="Exo Provisioning Profile" \ + CODE_SIGN_INJECT_BASE_ENTITLEMENTS=YES \ + OTHER_CODE_SIGN_FLAGS="--timestamp" + else + # Debug build without code signing for testing + xcodebuild clean build \ + -project exov2.xcodeproj \ + -scheme exov2 \ + -configuration Debug \ + -derivedDataPath build \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO + fi + + mv build/Build/Products/*/exov2.app ../../ + + - name: Sign and Create DMG (Release only) + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + env: + APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} + APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} + APPLE_NOTARIZATION_TEAM: ${{ secrets.APPLE_NOTARIZATION_TEAM }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + run: | + security unlock-keychain -p "$MACOS_CERTIFICATE_PASSWORD" exov2.keychain + SIGNING_IDENTITY=$(security find-identity -v -p codesigning | awk -F '"' '{print $2}') + + # Sign the app + /usr/bin/codesign --deep --force --timestamp --options runtime \ + --sign "$SIGNING_IDENTITY" exov2.app + + # Verify the signing + codesign -dvv exov2.app + + # Create DMG + mkdir -p tmp/dmg-contents + cp -r ./exov2.app tmp/dmg-contents/ + ln -s /Applications tmp/dmg-contents/Applications + VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//') + + # Create and sign DMG + hdiutil create -volname "Exo" -srcfolder tmp/dmg-contents -ov -format UDZO exov2-${VERSION}.dmg + /usr/bin/codesign --deep --force --timestamp --options runtime \ + --sign "$SIGNING_IDENTITY" exov2-${VERSION}.dmg + + # Setup notarization credentials (optional - comment out if no notarization secrets) + if [[ -n "$APPLE_NOTARIZATION_USERNAME" ]]; then + xcrun notarytool store-credentials notary_pass \ + --apple-id "$APPLE_NOTARIZATION_USERNAME" \ + --password "$APPLE_NOTARIZATION_PASSWORD" \ + --team-id "$APPLE_NOTARIZATION_TEAM" + + # Submit for notarization + xcrun notarytool submit --wait \ + --team-id "$APPLE_NOTARIZATION_TEAM" \ + --keychain-profile notary_pass \ + exov2-${VERSION}.dmg + + # Staple the notarization + xcrun stapler staple exov2-${VERSION}.dmg + fi + + - name: Create DMG (Debug builds) + if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/v') + run: | + mkdir -p tmp/dmg-contents + cp -r ./exov2.app tmp/dmg-contents/ + ln -s /Applications tmp/dmg-contents/Applications + VERSION=$(git rev-parse --short HEAD) + + hdiutil create -volname "Exo Debug" -srcfolder tmp/dmg-contents -ov -format UDZO exov2-debug-${VERSION}.dmg + + - name: Cleanup Keychain + if: always() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + run: | + security default-keychain -s login.keychain + security delete-keychain exov2.keychain + + - name: Upload DMG file + uses: actions/upload-artifact@v4 + with: + name: exov2-dmg + path: exov2*.dmg + + - name: Upload App Bundle + uses: actions/upload-artifact@v4 + with: + name: exov2-app + path: exov2.app/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index b3f86bdf..930ec3e1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ networking/target/ networking/topology/target/ build/ +*.xcuserstate \ No newline at end of file diff --git a/app/exov2/.DS_Store b/app/exov2/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..31bdd03380300e0306e73c3b434af8c3be94bbaa GIT binary patch literal 6148 zcmeHKK}tk13{7+vA6z&~!A(yfj3=l=J;C5wnfZevGxQ@UF1z zkBSQ!B!Tv&Y0~CD_-#W(JiTovL}Maq&;(hO0TJ`y)Q&lifLzx&rX#wi1!Zan7TQLW znuuuP!IEMP7HB{^R-g5$l}Ne3j?$Y+fGH>)GMc$xou%Uqp3R zo7p{M8y9E58E^)i0cYS#25@JKG<%BPI|I&uGw{KHoDTs_Fd7!cdUT+xB>+&K(N&;J zEg><%Fd7y`%s^N}ff~wIVz7q89?UNq7DWvww&H_r=l9};>+V=TRCnU2=)E)G4D=b; z)!|s~|5N-jgGGKn#7EA6Gw{zC;8B{TDL%^X)=!@&cWpqsKob$aA_@e0?-783oFfNy dQs;x{@Qa2;QC1Oq4F~!|pc3MpGw=rtyaA&II}HE; literal 0 HcmV?d00001 diff --git a/app/exov2/exov2.xcodeproj/project.pbxproj b/app/exov2/exov2.xcodeproj/project.pbxproj new file mode 100644 index 00000000..a4e54fad --- /dev/null +++ b/app/exov2/exov2.xcodeproj/project.pbxproj @@ -0,0 +1,548 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXContainerItemProxy section */ + E07D64CC2E36127F009BFB4D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E07D64B22E36127E009BFB4D /* Project object */; + proxyType = 1; + remoteGlobalIDString = E07D64B92E36127E009BFB4D; + remoteInfo = exov2; + }; + E07D64D62E36127F009BFB4D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E07D64B22E36127E009BFB4D /* Project object */; + proxyType = 1; + remoteGlobalIDString = E07D64B92E36127E009BFB4D; + remoteInfo = exov2; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + E07D64BA2E36127E009BFB4D /* exov2.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = exov2.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E07D64CB2E36127F009BFB4D /* exov2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = exov2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E07D64D52E36127F009BFB4D /* exov2UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = exov2UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + E07D64BC2E36127E009BFB4D /* exov2 */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = exov2; + sourceTree = ""; + }; + E07D64CE2E36127F009BFB4D /* exov2Tests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = exov2Tests; + sourceTree = ""; + }; + E07D64D82E36127F009BFB4D /* exov2UITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = exov2UITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + E07D64B72E36127E009BFB4D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E07D64C82E36127F009BFB4D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E07D64D22E36127F009BFB4D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E07D64B12E36127E009BFB4D = { + isa = PBXGroup; + children = ( + E07D64BC2E36127E009BFB4D /* exov2 */, + E07D64CE2E36127F009BFB4D /* exov2Tests */, + E07D64D82E36127F009BFB4D /* exov2UITests */, + E07D64BB2E36127E009BFB4D /* Products */, + ); + sourceTree = ""; + }; + E07D64BB2E36127E009BFB4D /* Products */ = { + isa = PBXGroup; + children = ( + E07D64BA2E36127E009BFB4D /* exov2.app */, + E07D64CB2E36127F009BFB4D /* exov2Tests.xctest */, + E07D64D52E36127F009BFB4D /* exov2UITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E07D64B92E36127E009BFB4D /* exov2 */ = { + isa = PBXNativeTarget; + buildConfigurationList = E07D64DF2E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2" */; + buildPhases = ( + E07D64B62E36127E009BFB4D /* Sources */, + E07D64B72E36127E009BFB4D /* Frameworks */, + E07D64B82E36127E009BFB4D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + E07D64BC2E36127E009BFB4D /* exov2 */, + ); + name = exov2; + packageProductDependencies = ( + ); + productName = exov2; + productReference = E07D64BA2E36127E009BFB4D /* exov2.app */; + productType = "com.apple.product-type.application"; + }; + E07D64CA2E36127F009BFB4D /* exov2Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E07D64E22E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2Tests" */; + buildPhases = ( + E07D64C72E36127F009BFB4D /* Sources */, + E07D64C82E36127F009BFB4D /* Frameworks */, + E07D64C92E36127F009BFB4D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E07D64CD2E36127F009BFB4D /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + E07D64CE2E36127F009BFB4D /* exov2Tests */, + ); + name = exov2Tests; + packageProductDependencies = ( + ); + productName = exov2Tests; + productReference = E07D64CB2E36127F009BFB4D /* exov2Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + E07D64D42E36127F009BFB4D /* exov2UITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E07D64E52E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2UITests" */; + buildPhases = ( + E07D64D12E36127F009BFB4D /* Sources */, + E07D64D22E36127F009BFB4D /* Frameworks */, + E07D64D32E36127F009BFB4D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E07D64D72E36127F009BFB4D /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + E07D64D82E36127F009BFB4D /* exov2UITests */, + ); + name = exov2UITests; + packageProductDependencies = ( + ); + productName = exov2UITests; + productReference = E07D64D52E36127F009BFB4D /* exov2UITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E07D64B22E36127E009BFB4D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + E07D64B92E36127E009BFB4D = { + CreatedOnToolsVersion = 16.1; + }; + E07D64CA2E36127F009BFB4D = { + CreatedOnToolsVersion = 16.1; + TestTargetID = E07D64B92E36127E009BFB4D; + }; + E07D64D42E36127F009BFB4D = { + CreatedOnToolsVersion = 16.1; + TestTargetID = E07D64B92E36127E009BFB4D; + }; + }; + }; + buildConfigurationList = E07D64B52E36127E009BFB4D /* Build configuration list for PBXProject "exov2" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E07D64B12E36127E009BFB4D; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = E07D64BB2E36127E009BFB4D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E07D64B92E36127E009BFB4D /* exov2 */, + E07D64CA2E36127F009BFB4D /* exov2Tests */, + E07D64D42E36127F009BFB4D /* exov2UITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E07D64B82E36127E009BFB4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E07D64C92E36127F009BFB4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E07D64D32E36127F009BFB4D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E07D64B62E36127E009BFB4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E07D64C72E36127F009BFB4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E07D64D12E36127F009BFB4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + E07D64CD2E36127F009BFB4D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E07D64B92E36127E009BFB4D /* exov2 */; + targetProxy = E07D64CC2E36127F009BFB4D /* PBXContainerItemProxy */; + }; + E07D64D72E36127F009BFB4D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E07D64B92E36127E009BFB4D /* exov2 */; + targetProxy = E07D64D62E36127F009BFB4D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + E07D64DD2E36127F009BFB4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + E07D64DE2E36127F009BFB4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + E07D64E02E36127F009BFB4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = exov2/exov2.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"exov2/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + E07D64E12E36127F009BFB4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = exov2/exov2.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"exov2/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSUIElement = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + E07D64E32E36127F009BFB4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/exov2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/exov2"; + }; + name = Debug; + }; + E07D64E42E36127F009BFB4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/exov2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/exov2"; + }; + name = Release; + }; + E07D64E62E36127F009BFB4D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2UITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = exov2; + }; + name = Debug; + }; + E07D64E72E36127F009BFB4D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = exolabs.exov2UITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = exov2; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E07D64B52E36127E009BFB4D /* Build configuration list for PBXProject "exov2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E07D64DD2E36127F009BFB4D /* Debug */, + E07D64DE2E36127F009BFB4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E07D64DF2E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E07D64E02E36127F009BFB4D /* Debug */, + E07D64E12E36127F009BFB4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E07D64E22E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E07D64E32E36127F009BFB4D /* Debug */, + E07D64E42E36127F009BFB4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E07D64E52E36127F009BFB4D /* Build configuration list for PBXNativeTarget "exov2UITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E07D64E62E36127F009BFB4D /* Debug */, + E07D64E72E36127F009BFB4D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E07D64B22E36127E009BFB4D /* Project object */; +} diff --git a/app/exov2/exov2.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/app/exov2/exov2.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/app/exov2/exov2.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/exov2/exov2.xcodeproj/project.xcworkspace/xcuserdata/samikhan.xcuserdatad/UserInterfaceState.xcuserstate b/app/exov2/exov2.xcodeproj/project.xcworkspace/xcuserdata/samikhan.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..92fe6a191ad07def4e420426fd8d1b86bed38e1e GIT binary patch literal 33984 zcmeHw2YeJ&*Z15zJ1v1^Q)me(q?5Ag4bmIwZPOF7*=#n+l8_DAgeo{!5F1EQL==Q1 zhzKHric(Zi5X1(EfC`EYD}n_@1^mwJWCMY~^Z2}e@Av+`-%H3QnYnZ8Ip?19Kj)sQ zZ*Mi2IzvK^Qi#G7O(`gbVkwTYs_>es?=TsRZIxcy4qdYW{wne6G`3fG8S5wMb)6=A z3e8`mQQGFKbM!Ox4at3l4pTfOl&U+moq7>%wxew*YibBJf*MJUqJk(D6-Qom7uQhy-|(MW-KWQ~TPp~wb}M5B-saz>+(9~z7NQ2+`=VJI9$ zphz?x#i1mWj3%H=l#B9EJ}N`yr~*}^TBJdG)QH-U5w)Xx(S2wET8QpPi_l`U1T94m zpa;=1^ay$ktww9mTC^U$h_<60XeZi*UP3RUSI`^iAbJzMg^r>x(0Ay2^b`6O{f7QT ze_;l**b3WVTkMFPurqeWZrB}1;7A;Wqj3z5#pCdJ9EX!|GET=CcmmGExwsgsaXGHQ zRk#{!a2;;IlW`}Wg>T0qo`>(m_uz$i30{gH!preS{1o1VpT?W;RZFW}$tWqgH3G^Saaqj`EX?LxcKZnQf+hW4O6 zX&-tl?N6)d2s)CErN_|;^aMJeE}<*vO1g@!rR(WNx`}R~Tj|Mk8*QXb^fX$e@1hsd zOXy|v3VId&JiU#6fqs$RPVb<1(!1!F=$GkN=-23j^qcg1^ileK`V{>!{Rw@V{+9lZ z{+>Qd|49ErU!;Gh|DgX=&k8n32pV#))xe#xg;SiV0)FnP?`4DP)S6Vn)rBFr`cx zQ_fT{l}r^=%jlU#rj0Q&?aWMO7IPajn<31d%tGdVW)ZWPS;DMf9$~gJ&oa+3&okSY z7nm2B?aU5lC$o#$&Fp6mFo&4K%n{}!bBg(x`GonL`HDHmoM(Pyeqw%R{$wt(!`Tt+ zNOlzK#5%L1Sr^uobz|MxF|0Qm$VRY{Y!sWsCbKDQDm#JAVbyF2TgsNPjcgO!%o^B9 zYzy1UPG-B`C?%`!V|o`#Jj+dyYNN@tnX}an{@rZYXEN*>XzGj!`u<>J?<#?K6i{e z&V9#y&z9d?{bXm-7{TC11r?^BTU9Z{v-8J3pPD!O!I9@ptm``Mdc0_(l9; z{$YLv-^)M2ujilQH}QM;SNYfYz5G7@b$&m8fPaI3i+`6t#2@89Knn_i7X-mdun`=EVZtcENpKaygh(Mqh!x_6R3S~s6!L`vp-?Ch zs)SmhSuhBbgcf0n&?QV4<_LEP^MwV%LSeD6PIy9CFFYx15H<==37dqch0VeRR*V&E z#aUUEdbM@6w!TLVrEDl$N=Z3T!^`v4suukWQx*K|+iA+Ybgf#GsfV(o>_sfnJ(MFg zOjHomCi-ZUUXiIWQR&ecVL>Ue(IG(*QE9P3F_9rDL6K?EVIeUQkzwI6$r`0&R$6+w zv7^P*uGQ(&jJmGL`nFC>%TAOh6}X;qrbbgPlq=;%xl>~(50MdBkrR1Q5UoUOamaeg zi}HqVU-kZs=@Q8`_#$_3)5J=>`Rpb)B%4_fG^|Pc^o6b{Jb*^&J}J zsH;^Ojji${qoq)Bwh(@!>b%$U%jZEwDM)zXLl7A8V-=?#XydM%@`plYeW z$EixHimIk+sEMMpI9hZOT}8LYDGgOeX{mZjC%TI&Q7x8;mD1?kD|E&Ny$Y(!F?6c> zyQb=2ESUM56ft)XMy}Sk!U**Z{SSj)H=)DW)h^!vEzqP-9oqlT|pBl;YnZl^?wPq^xrx}1jjBVPE89h*HZbiPjz^v}d95|pqY9o*e6tuK4pSqhG-c8*l zj_s!I5&b3EBysJa&l~}9w~)G@TBK1r!RkrOUMMNz#rpORy$K|fR_cWqCD3kwd$!;i8k};<;bp;^?70*3wm1%hjJ9;HtYF^kz&YNJe8<2aFjCc-r;V z6|)Emc>DHP`_z~lE6p-Dr^HZbtfiJ;L%IW?SfC2(k!Y^4!zfK%p0-U3JgYKIGcLCXUJht3?a;N(n>4%wmM$PIZRU$8Nvz{W^J z87K!8fHl#KTEL>1hD07^c|mQi#6?eZFBy{4pdLTXuPR9acd)iyu|ghgHb^O`10X0+=~(S6Nn z%G8?h(5vOl^r8lF+}4!U00d=dG=S)ohq0V`7=&oPIhM*L?4_1VJZa&#N2qQp za2@q1wUSyzJw~mj)`-Dkh!`q{iQ!_zI;w~2rPjgn#qcg>9~3 zhSYpphOr}GKdn&P1=7PjK8-RI%4KS&>a*HFP-t6QXISb{T3_RyP$0Fd1D@#2)poV%n$_}T7wa2g>}@(p)*mq|e|xF@J-{fhi}7ND zm?#S#S$ep8d%K6qr12*87Rcy6!cglwd#N|6x2d_M`D*w>uIF9qa5r^GOco1|0nZ(! z-Ums0j5-e7_W^u=M4hBgf%LQ}fVoDUS<=>CrNTmxf-sx?yJxY{*a_Qdp>{DWxG5Y7HGd)Q^&sxMdkj zSALedB4$Y~SClkM#O5Aqxwp@JFJ8K5xpc{VDOc|3sJI46fpRy=^x@uU>~J@W^8|BG z5keR&gXE45?Tj9TsJRZe|0VyEZ!?J1DE(j{GK2O_?bT5Y+*f-LCk^V~E-$wsAgkJb z;xqT87YR~dD#N4ok>Q{Lhroo-niUf09uyifJJ5YrSaevRJJ=$#Xa5^oS|95pTjW3m z_97+h4fbNGSSFVDQd%^OssfF#0+c~ub?<&k8ldr+H8vG2V3)45s{`~Uk6V??GMT{S z&?v{;s*suS|CZ{9T#yH-e#jN}HFq>dtP-om8gb%U#h;_GA%Rt#myH45S{&OUh7fL!nc!+4d^^g(HW4wI*WqbGip*EmA**iE6 z8{TKy))?FMUHSyXW!mr*P+*eaFCVVJ_+lpZC#uFfl1_)6xhrPkv%eU=qRUatm-u)ePpzYtJ`3Ta6hZLy7z0q@VtlT8j*pA zynU``1aDV&xbN3@KOhjc1`X&5a(7@O>Dyz?Ta9@zYlBq5?*FnB2-mCClt`0uwT2t- znJ3I#J7CauloRZdV*zQ72IDXn&}B8)Q}tkwbb=DG0`TH@s6!wIkHYS7f;t7V^c(6s z(9He>M0f}=;Yc84SD;{T(9B{`JW2rcHy!k{7C?8`%j#FZJ{B@S9i!0RTWJ0kHjhFv z5?Mxzx^5IJHr$dd!CFA^)cq*&Us((K0_E20>6ia68VlCq$ye8`=@EwXSXmFE8V|L( zQ8ni6yi{>HC8cGRHTt$GGv(Oo z#89BTc-@pJ?f&ai@=*^r6buThA=g<=-PW?|B3Vc_wnGN7klu{>{>kUp^w3o*gXj>+mx@O+9^nr&~^lW_kh3h7E$$ZU1bOorG3hY6Js7P!T zC-)#VDiPa6qa=%cK^5!(GY|B#4$vJfn1^{!NuLfzpQIyarAZscfRd?Tdo}^wacvp1 z-hwKrFV~29^6xd&;Wc7m|Hv%;sYCTtU^mi=9o>`{sIt%xXGuRbNt$$@;w+gjvJ^B~ z9&19Aq+v88gV-r{^#JhWYX;#YEx7;jT)nmh#G+Xb2kfh&xc^R{wk@G;{aT)->M3Y8 z6}S;~AQS3DU1%y;VAIhIG!xB2w~5ol>EaA=rZ`KyO`I*x5pNeok!(bBsI_3G5p)Nb zYV**YXg<0N-7Ve$ip@OnPI10?7eVm^r4m#?P$5B+rD3{TH0G-ikQ%J~&OXIUQbGMw z^q>XlB@;#4VK5on%*IA3EK}di$dAe z57dWXQHNdE4~x;`+1_E83?PT)w%HTdFz|5zYh(jK-(enuv|_b++UbVLhME25g@f5t zFn7aTSO(MF`??98GqeNXDNRA4++wbFhOw*jJ@gP-4&v`&>MU9T4dtu*XDC&S}?=Vs2lZAfos-?K{Dw+ z4h98UC*CaScqJ8Lf^t$*U zLALN;B1(H{B9cvgjrS=SN(-gJP4nCX9F#T#a5IC>az&n~x2Z25NAI9_(IIpg9TAs_ zd&JY?X9OWJ?*w`u9Ye>_30TPw(1++FbP}CHAEQsuY4j=j41Erpc|#v2KR|=eH-qJ4 z8o~aOaBxXIQkl#a%Jn2@Ym!{fSHS|1ByHE4S=X1h8*o=@r`pnk(&Air+Va;`!PXB{ zh>wWt#P#As;%@O3u~&RzfTfAPL}$=f)JF&`GjWypu($>cz~y3wMj70%X%85qZ0lWx zYx@;=d0Nj>U-qIO&^dHoTp>OxuIxAc&`E$ezJtC*N|v5&G9ZDSCe?HuxJuv`iPwg2dxjm9nja{y`f;L*|?jnWw> zt6;WU^{R$Wm3hqp%FwnNo4~N`Cp5`El%{Owd|KQrKC>PN;6NONRX7-j;7}YUZV{gqpBJ}@FNiOSFNrTp zGGxTH=<_v(NVN>zT7*i2zEf*xm3Cr_?`b3$39|B`HiNvmAOg+qm5C-@t3d~#M3C0( zr%}o^1xa{$-z8fK4bK1p0&teJu}m~VxQYZ=o9~pH24+pw`;Y916{vq(*OXk7q- z;i-5Uo{ndTd&Sqq1L7O~auDmyK+--LNE!h;thDm(*GZl}d5DR*@P4`Q^?r@NCp7Pu zgr@kWEFN|Lk$CLutan&QpHdop^~&`FxJKwKGYgovHOl|{t;$Lfei#GTz816dA@Q(yM0`&?D!#uS_uyW<79_+v`~+T)pA?UYC&UjxLVPHGBz`P@ zaw`b|M%jO>RKU-IHloJQ*BUz-0F3T~T1;RN9hdpw1^gn=I_v~}fOCOd4xr|>c)N@| z-{ihbwL7W(Yb4olYYAZC$XD?0fsFqe-X}5sUh!l%eqB6ubH+c2%hrgyYGtt~iOp*n z{T=+ST@mrY{<8Q)>|CWAY)-q@cREzI5%8=_-i$SWms!&{`7SN2DLx0dH{|lY@`x`ke z(DuNlv=#QGhtNZ58`_pu(stsHz@|TmKa0PJ7sOx1-`3L(v?DzXtLPE*NO~0QBwiGM z7yke@{ZqUo{zVY=KZ#Ez_Ym!6<|Q*l(Y}9&ujl|eZ~$M?!E`9_6&)g8?xw@UD>vmU zI;wv|9h|r5@pPPA^$q+(C(=pO2|5LxhTqcZ41ySfhC<*0!s2f8AJzof4B#R5#(^w#V7O~ zR~LUIL5>3#|88mV@1gId?;~g!LBk0eag)XW&)2wbBcdOppQHje(5vY+bT{2Y_tIH4Q1qz8USZH^(tdyBNz^Y)pTtsDGuXiLrU)I zY&Ld4LpP3CGF3B-tqoxO0M30wKc+#5SuWHp8vyif>dS6=4?*62s4Kk})Mgs2ed(Kk z>kY&Ze295UrDd((qTi*4_t0feyXg6r8a}1Jq=xs>pV6PwUl0^bPzXVxz4RISEBb4K!U&2WC{pUM&kb|beqATk zfUCh$1mB`yiE&1n!6ZF+)lLIf)eTRW3=MiPAyw0)SivLo4S zjX+J;Pqeu^3Rb}Z41B%S2Sl!G=~3{K**Ex2Fh?zjq(-Kc0o$6wS~29RWJ@6^VSr>) z*n?zKI4B$yAl4EIN+Kxv|CVG^_$dJ9=vIs+D6Lxol$b8<{dS<>_rum^c*49HD1xcO zJ&F)TC_%tBx&7#`B0>RCeM`^l6nd#St)gC`Q#24%Mo>9H z6$DkTg{1vf#2cz;RA}8YnQ-eOJCa+GeB=vOi^?wOo~oLmtv}7nqoRZ)dbZL zG?AcMf;0rx5u_!ke!aZUfq&V&-}QN)Y+)QyD7f?mbQK^?jwKeO0VIjZ*s7OSCf88k zA$e3}zrA@Ek>kM!EnQ;4VF#A?zgg{tS!pG3Z$RaNH!GY6C|m}Lbd}K`R;(P1^Hu@pttO~h;yi=s)6XNGNizxuaPC^g`oW%u zsCC6gf?A~KTScF%tOb#!ytx8!#K>Hxk@&B~Xl6dk(6%T5CU{!0Rq?FiImPpeZHgBZ zFDkYxb|`i#b}3#`ysUVIAR|HT1c4&kL6C`{PJ+4!no7_#f`F!H5HypZSp?nov|^9q zRmE%Q5VcnEI_0Z4pm;-Z5FJvyrFdJ?+-4JWFTsbvi<3d{d!V=BqXZu#_;c|z!9NrH zi_EwqA+!;kO|tB_z{~(tOI+LMc$Dv%N!%2SX6E`$biZ7>jUR!JdyO2-tC7cK2Z2*);=?}5hIAa75f zP+xAK(-og8K2v;-Pbt1soKbv*KEo{pSraseAQAl0;#q>`5j3BGJGvJj*Su?tFvSnD z5q7&q8TU6#JF{;mxxHPsx2~=4XSqJ2QBIp4WOlLyH5g2iV`6fB>kM!lnIu(0(6?7&N{=gwDrY%zR1)&c`&zM@2@)hQ~yQMny)2g~!ImM2Cf5Qv~8j;n9YO`p~GT zkZXH$QSJ?BElGcObShYJLuad*Dooebd0DRWPK`49->-8}c!pCEiyY!nZfU_wz8_hrz+%WH&7a{G0n*hjanSX?uK{Ji zb9l=(WRxJ^98{-Qn>VwoCP1G!W=1m}(v-L`u8f<+js$_feS4phpOLl%SOats>|#f?!`> zvw=yM)iNfF$!2mWUnWo5%(`XGjG(myJwwnI35M7zZ(uhX*8dAb=m17((90~Q`dcsr zQw=Z#16p%W9}K~00ES@dDDyW#y%G!o9{@w_vv5%pGYL#qrkODi1i;|B9;St9CFlu) zHc7V4zeE~tiBwn`nZk5|KE`w~CI&Pj0JEMXXhSbEm6;}i2pb7{>i<26z|5g+ncHu~ z0PfGc{m_BUDb|nr&Jc1>eiA5w2+SQ&cJ7U3tG-d5e8MF^aLdp8qaWJwLjaVuOu>BS z9y1cd+-*i;o(5=uxlcl3Hj6&jqc0w>cqWU1gP@%R?IP$Uf?nRhJkiI; z8=0q=O~A*SB|d(|!pE-?1fbQs5+5J>Up_V;BvqLJA^oS|MCK)lk6$Kew}p@QNPPS% zT_$}K1VAGze*!1lTUPrG<}Hbn4>E5O^cq2XdziNwSf+ghz4^~^@_Wp2iIa~q?=!~; zdYz#C1RdyQPB0%xocsns2mc3f@@e4CPjBSTId3k0|F+L_@}3(uaZSME&0EZz`~{T# z^2V~y>`8X{g31WpboZJzKI3+qYdHC9<~x~_0S*KXe@kM-vl1u2eIq9iY4%L=kXc#D zS{CXT<~Ny>e>HRR;agN-dE_!HKx`~?h53u6Sj1wMW)&>MvMk5)1ieSlQG(tl2$tkH zK_>|MfS?Zv`iP*D1fAN*TFLCp+OW2)5~47CSqF)sKejORX@b5d=q&6Ez|w#H|Hsl2 ztRioyeK8&qh?d+n9Zc8$0NXw^=tj51f44J%^#JZ?Jqh}xkNa64;C@&&^B1g<`~}C* z%t5nk5E}yI#H!d}f(XiBj^i)z9i^OFB`|k zOML$oLGbzi+6};_QMPRQjl8^g_O!k8U2=3A467bE821!FG2utpOemXmW7&;AM9yAV zl>F(FRY^;>ynhT7Tgwj4=CTFSJm=BCViN23u!RzW`yHUDH|*f(j{frnmSrkuA-%vl zwt}r>tJrF`h6QNz2ZGKKbe^Ce3HphkpVzS(;7u)C&+6C)f_{ zCma)nBng%$?qKIq=KKSeAaeFD_Fl96W$%&XFQ!2LvW1cW|L%r`v{bT~eH1h)b_u(b zeSm$CUB*7dE@vNRSFn!|j0vU*RuIe(%o5BI%o8jSY(=p326kni=v^Z_W?48EP4JLD z!HZ#EiIN?&*y;a2vHL$jmTr|~>9Yh6waC(KH*w5bR{AA&w?w2bv#$^gu&c6%-NU|0 zupPmUk~IBC+_LO`_Mp_r0T3nFUJ@nrN9d2dxQloj2%P^8^ou2gas{(9YhNo z#DQy>g0ES#7Iv8hQ4CD%+{3;_e+2Vqc5Pp0P+6Y&k-a2w@lWi}>@VyE_E+{d_9FW` z`v?0c!7c>566{8>JHcZJ_8{1kU|>~mf_*lymt`*IkZccejKszA$tNHZ&;s9)?I9ff zzg+y!+e4fKa53jduwNe+b0dI@xslu`>6_rO1pCXMU=Iyb3VrL%xpSVtX51JKRx^;` zpdQYPgY{Gq9Qu#(GUv|)NsRNLaFQ>Zi{zrXXoABC z4ktLGm;Ra?$Bj4hFR*Zw)XRSn{P3Uf_`eydafC~zY`K&hm84I8*z(>F?_`bN6t!iG zbGjYC0pUlubSRr~W7#h=ygzjKAnD)}!DlW7H4Ce*DpXt+mkTpTU*>YmEF1$Y%)#0? zz~tQ^1ufH7%z-1lhf^y)Cm0mbJ9@Y>uAJaF0;d74g>|_aGdPOJYn0jl5jbjw`3L$V zWPO{{a5^~8#no|IuAbm{f)fZ%>_uu$&ozP;fs+VMkzjWzLx-j?H%ncjNl(ivDJa&c z({s~POR@^`H96^(gB3`xC`^V!bCv?`H`Gv;rOrypO)rLOq&m!d1J??%bKGQtA@PQM z0fOn^$S1hSL&`RoUVt zku`CPqtfDWvK+<&&3drnX2sgrWhybN%5oK{aQ+60`wUXMM!t4!X_-EHC2_iz;9@cF zDE9ey>pr9q<92X6!D~HWz}HV;FSm<(NeZ00b+N(E zyA9g3WdysqJp*EzxxL&$a6@tXxYxP;+yU+lf+rGOOR$FEI)b&2b8m8Qac^_)aPJZf z@-KL=H%C2PP;Q zrh+6imbRjAY|C;h_QqRYeLcM4R#0JUSCgTw)MO5-wbT%EV+}qnHyMe#F9#jOF+d+J zv;7NqL8f9^AY6pwblmR*w@GJahNT+YXSk;TcF?MKhwxH4@0;bx%iNU#^v@$654^ze z?7#~GZzW$SlM?iV%r>{l+kg<_L3!@FMhNltyaVsZZ6tU)!LTJwHH)7AqNJ9DP@LBs zKa1+ckK$cGWb;nEGe4T(83fNHcvdg(O6}u8`M*tsEq(Tl;^=0ia*fRHqwyO^MINxP zG+;l1=SV@8X6TL&;0GR%;Dh-Pd8%OZzR7}Go{iw6q(H0BAl{^#2Nm)T*y#GI9ml7G zox_jku#`v_jJfzOcb96p=R zkziszU$S!+TF{C`1V3uVi1Et*HFN%XGl#E{C5$Bu2PE@`?~=@$2M0B8u#1HRXYsd#dBfkv&*tY4 z{1Cy*34XYj7kL7BF$PWPk^lF2F@HB@%inXO$&%D_sCa=>&I?PI9h;hw{N(MDdBZP& zvI}o4o31^##U1CM2Rm%;8M!THFO>CsWgza!FX10F+c`YUUo63^z|P^j%y!O$H?wp2 zNBGA;M&VQZN`4i@lHt_^clYwE`89ku!My}OPw;^Onk~Oh)@FSa zJ^y`&Uiqha$ildu-^@S5Z{fG{&+^al&-2>|UQ6)f1g|6b34+%X{3O8}2;R7!f05tL z@4za47ylCfGXDy}PZ7L{;HO1+W;4Oh5DXyTbCNdjKlPM6fAH!#JO1szbIy)G%pV!J zFHYL=pYop#to%#<2We@}U{C&Q{u};V{yYAA z9!PYX2utw-!7mcLo!}h=?<9EFdj1@Lp8pY6=4bvF{sR9i!7mXE>$68%nO6yZjo{Y_ z-hZo=`R_iRES-83C{T$6h!%VKA4^&wFjAE)!Mpz}Squ6EtY9q+8L%vZtzZXhE+`4! z+b!4=yzi!KE)4Hq^BdC>2+qRjffc(6et%=Vg|ULa5P(%ekf0KRg%E<@Aow7`Z%XS8 z8^_xOzeDi5kmAR32ps%paOzu{^QgZ=^};w|{J@ST2ub}jARpTv{P3udE@TXRGE2w- zuab~W@ZoMDm%w3dA1Oz;q}fA=Jsbg(GmFW+EP`E{vV+Wi7Bx;N70RJu=~}l?LGb&1 z`HO^VfHTY`Ev_d)Bj{jT6zT-60Jb1(slc2c^a>4vUT7p3u=`U4pEjd@(EJUt&~mu4 zWNT=Yk%JYG!ki&TOvAuf>697rgXZU?l=U}MJg~gffQ8Ijg*Itu`^5Ne0Sv2;U}!=I zER{*otQOV?-9nGh3nz4j5_}ds!1xD(&k+m_{7CRmpm_UK8BFP|h9(1?*nvRx zPBR*m2B|l6kjY%z37N|#!>OBVFK0ngeMpc430^^#$~V+$Ip%XYZBnfJwPh{#44@$L z?_ucFm&q$vYW@KUQX8grHkkVyWcKm~K~kOe zF36xc&irSy5h&TbyjAjZp^>W4@;t3BG%7Gk=EH4fY|t|2+l80m$dj-`*eUE1V2imx zFzhwI5qxp2@QSco*dxGZ^E<(R5Dea)OVWZ52UZvu_Fp0epUW8}2i_=?U&u7rt%7zb zfiO&L$PLx?oj8yZ!*xxTQn|@gsMa@`Q-qb84aL&XsFa8h<+t*n-l7io3U3SV2=5Yn znb4TfZ2z$s;fQbyltSS>;i&LF!GL!BMQEy*$rersaP;IELc@ObyEHP1$p;Q#K+BNk z&Ddc+v>~08x<$bhI5wcxLk>Lo%?)F3U0QC#d~B@9-LfvHg)au_GpiBK2oN>$3!!O3 zE5slj*hvYwz7Iw38l=Tn?$I&ih{X@o$kMoRA z&dSco%~O|G)M)zNj3Q@>g&eGX!;;@r07p!dJ3&y@1F4!|4;U%ZmFg~d8Gs(re!n+t zxDDj>m-^l^@I5NsBSl5GlgNk;AQ&?s_0lm{H}^3fHlAMIKE8f%O%3__psmJ?R&A5{ zWeEKz=pb(_oW?MxV}f$hmgJe_BL`O4-K`WkUS!1)K+GyQ8Uktj%)#=dSyCG*aNzVk zn}9&b_X=vS+%80S^qtJ>r(37%?_4v$;r1e$rx1B*WTBDHz3`S1(fmr5-l*sp^P5+O zi4K6G%2h~%=zkc%wS0AIiPVt{y;eFc1V@XcjO(Urij1@rDa|rtT>ZC>47W*0>P{4G zL`SLZlyv8w)HJ{$lsy>}MDy!NAov7ox6~}XM8kMZ_FWio|6S>{;ng!uN=nHd$uB4@ z8Z7%REbrAkc0Gl~S5xrj7nGDP2`%+dgVP^@cf>&QVBl{xu{Uzk&qFeLV`^!6s#p9s_Bk<4`J^2pKqULj)3R z-iz*s_o^+0cd9)M?^Ig}Z&h1^wn0+CU(oOH#snoD{n$wwC{vtnk&%PPsG_!!!`uJzfObYvaG1t?H4*YPmqEVfT6PXQ4`sV` z+0A~%UV(RmjfQuDxpN-iRQG}Rf%$WR@Gh`KE}2W^(zyxX-Ob_hxB{*Tyo>9&J>0kO zo-RM$z)#}u;qT>_^BdsJT2F&{zZFdTZD7Ce0NedtFlFE4!FuICfcI*hf;Ve@%74!P z3UASJ5kla7Sw%vVFjKf2bgDI=Bdvw^Vy%aFVm&XsAZ&+y|0USy_XuCWTdqb}jkPMU zshism*dDdpP&D%Eb z+8nld&*pub<2E1IoU!@Z=3ATZZGNyhZ}XGQFSfR}V{KDyYi%dnF0@@_yVCXv+b3-| z+HSJlY`evFm+i~8yKP^!-D~^0?E%}wQxn8+N`Lc4K z@=fJY<#FW)%8!)4*l~8l?NoNjb`$M1c3L}~o!+j=&S1C7Zk^p`yDfIl+C6Xgg57qz z*X<759k=_y?jyTXcE8$Pv`6;t_TKhs_C@w;`%?RI`#Sr{_D1_D_9lDL{to+j_Vev~ z>^Is!YyYDC9{Yp#7wrFV@ODr+#5<%pqKyddCLGM#pByDUK$`F2`w(GaP3*E_Gbz_=@9)j$b)`>-fFnImaIz ze;#HtqHM&J5pzb65pze(A944Hdq+Gz;^`4@j(B^-yCdg~Tr%>(kq?brG4j!ot46*! za?i+*N1h(}*{D;a&W!qb)WuPMjJo86oMcRu0#`Dk{u>*%!66{DL* z&mO&I^rq2!Mjsu0Z1joIpN>8|`lr!XTm%~h-WtjjMhzq(v>`NI{t z(ypwl;A-va?i%iz?wapf>00eN(N*KBb!~8MbiK>9*LA<^2d=-lS-XvJi*$>2i*+0C z7Vnnmmh6`5mhM*TR_9jl*5KCY*6cRPZKd1eZcn*A?e>h@R=4Ne_PD*~w$E+9+XrrE z+`e}E*6n+@-`)OnyX^Lt`w;h$?jG)5?mq5*?j7#4+?TjNA zGklE8n6YEVkI5WUIHqPy?U=eT^<$nI^Zb~dV|I@@>A`t8dU$*IdW`i5@Cfn<_6YR| z_lWe!@W}MY_Q>_f_bBuz_9*cv^QiFX@R;MV#N#QC9UgCceByD|<5!R0J^u8#?3w79 z>6zo1=UL!s@Vv)!spo3XHJ;s`y`Il_KI{3s=Zl^@Ja>7%<9XckL(h|*AA6qm{KoSK z&)>aRFV2hivho_?j73Zb)>hzlGHQj5b*KJ;NyhN`%yykhW@>=cH?bYk`xYrY2 zPkL?j+UfO@*DGFoyk7I#=XJ#EsMj&C6J9@g{pF3kX>Z1Rgg0np-hSTx-qGH%-s8RF zy|cY@z4N^by|;P4=Dp8*zxNwH!9H<5c|HX`MLueuQlE04N}p<`@8QIKgv(=Q~C|_i}H){ z8|N42m*AJ=m*SV^m*J=J)B5TB^nOi#2EP`+PQO`xv;A)OBYt!J7W*yrd(iJ8zx964 z`@P_|-EXJg0l$NOZ~49B_mSTzzfb%=9Xn>M|JcB>sZL@ z*ZlYTzwZC8{~`Y){y+Ku6+i``06IV!;1Dn@U}S((z~}%~Ky<*kfVhB!fV_aJfa-vn zfZBlD1MUd8GvKa(djl2(EDBf}uq@!|fGq*f1#Am=FGC8FlbTG(x7EQ4+m`tdM@aNpzT3BgI)@H zCFnrVyFo{S-VZtv^kL9fLEi?Q4LTQeDd>udQel;?%1-5=8m974d8>R?V^vYA996!m zNL8XLS5>Jds_IlaRinzFnxi7BxvD!=cd71GEmSR5EmbX3Emy5jZBxCk`aRe=I5Ais zd{6ND;Qhg01fLB)AN+Ihh2Se8T!>Z3kPzDtyAX$vVIdwNz9If0K_Ouw68bSNq&#G1 z$Xy`|LKcNA4Ot$tB4lOAV<8(twukHrc_ZXd$a^8jLOuvN9rAg|nUHTnz6*5>%?ND@ zZ4aFtx;XTK(3PRRq3c7pguWblIP_@fsn9c_--P}e`e*2sFcijy@nP0sL&F@xJi-FP zQo}OC>cTq0?hCs=Y)RMyVavjnhph?$3_Q62SrtFh(6y8dDKd9WyaT z6EibrdCc0Fy)oa#T#j{!4T(*Qt&Z)Ay(@M{?BUoCW6#C@6ni1|V(g!>m&Z}#=yB{g zew@{~A>&4ka~|h1&V8I_Tk5YTWd=xpDL3?ulCvw+u zi`U0@#Ltcw9ZW zIiV|ITEdKk+Y$~Wyr1xG!r6rL2|p+Nn(%wVrG&o{aUzq*C%PoMCwe4$CHf?eO$5Zhfk`5;wO*)?RLDHq;mJ|S@ySWasmU41naNXr(GXU6=Y+>iIO=G*wzoT1i@YT2)$2nm(;9ZAw~a+O)KpX}6`# zNxLs?aoPiE52Zbt_E=hXT5sCxX`iQ^Px~$HkF?9_R63V#l|D3GnQosxK7Dff{prio z*QURaz9ap$^h4>Nr=LmxIsIb#pXpaJ=nN)<&#=lEl`%TQJHt04A|o~$M<8QU_pXY9;)DdUxlS2Oly9LPAB z@m9vW8HY38%Q!#5c0%}s(g`yrte$Xi!Vj54GXpcTGK(`yGb=KyGIg1)nQfWvnH`y( znNu_8X5O87U*`Rp4`e=+xgzt?%$GAi%KSR>T;@-i7cwtq{*{HZm@GcaDl0ropEW;g zQP#?=%~@Nsc4i&O`Y7w;tnaeUXZ@V@Yu2AxSF)*WoNbrwnC+77pBuRCvT-nzWac`xSe%zG(sciyXcd-D$G9n1S5?_}Pm zd0*sxmG@0Pn?E)`Dt~-_LVj|7Mt)X)Zhn6L#QetmrhG&G)co1`V*cFx`S}a;7w12a z|4{zJ`3LjAD{w4uDey0dFGwoLDkv*36tou1D40_~3g#8uQ?Q_5QNfaeH3gdrUM<*H zaG>DLf_DlI7aT1(UhrYTse;o57YZ&G{84bJ;7TDXR1|WBLgA1?n?hw_P+@Lid*RZ; zZG|U`s3Nzbgre%AhN7mTNky$iT}5+>NYT8ayNd2DT2OR<(c+?2MLk827p*VaRP;>I zvqjGr9Vxm{jElKqtKy->_Qk`BM;1F3k1Y-?jxA0qPA$$T&MGb}Ru`8QR~AqJz zUanrDUa9^@{j2(N2~|Ruuq8uFY)kA*hLwyci7BZpxvk{RlDkXpD_L0bP|5O=6(!G> z>@L|~al0zlOOFk$$S@KEAr=_^mvoxc$ptQKOw6wgmu2fs9E1g|>Z|S1arKQVC zA1Pf`x~8lzmqAW!cx|9_0b$G3D{)N#&{KndLdb?Nsr*WXb;anitDeoe;H@ReP#lt=e1lcGaP(_p08nI$QNe)t^LF^(WPz zR)1Z6uExG5uqLu5re=Iid`)&uPEBr2eob{vP0hp_P0fs&`8D^{ET~yjv$W>Hn(mry zHQQ@;)x1*kYR$fy12u2fyi;?y=4j0~HQ(3#P;yqkH>oV#x>&ojI>RRe1*R|J~>Sor> zt`qC#*4VDHs)K1dQ($3Km?L6&W+IzJNwTrb6XxD0= z&~DIf(r(sn)jqF%QM*(7lJ>avbM3G7cJ*%cVfAVC#r4(oHTAXiP4&(7lj`rSUta%k z{fhcW>(|$Btbe+GOZ{{8+v*S0AFe-If2{sQ{a5wB)L*Fowf>@x(GAy)(mCr~bz^j1 zI$xc?E=ZT4OV*|7Cg`$sxw-;fv945CuA8Epr(3Ptq}#1KqC2hoM)$q$obJbl@P@dC z#DKf-Y?rQw9@u$WM zjlVVi(RjJ>uO_}p*)*zYbdz^eU{h#QLQ`^6YEwp2R#Q$>NmFf;wrO$Gil&WCTbp(^ z9cX&9>7Ay-O-GxKH8agN%_EwfnthwcHODt6HK#UbG-oyEHWxG(H^Gb?{5fg(q^wB=lS(I*PpX^LG-=YL$&=bAbxgW_ z()>yHOj`eKR2E+eq;RJc+U7^`|$Sh h?fUjb?GLwiw{K|Q)Q81FaJcl(%3A(u`D{P(e*o(K#H9cL literal 0 HcmV?d00001 diff --git a/app/exov2/exov2.xcodeproj/xcshareddata/xcschemes/exov2.xcscheme b/app/exov2/exov2.xcodeproj/xcshareddata/xcschemes/exov2.xcscheme new file mode 100644 index 00000000..b6afb5a7 --- /dev/null +++ b/app/exov2/exov2.xcodeproj/xcshareddata/xcschemes/exov2.xcscheme @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/exov2/exov2.xcodeproj/xcuserdata/samikhan.xcuserdatad/xcschemes/xcschememanagement.plist b/app/exov2/exov2.xcodeproj/xcuserdata/samikhan.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..f9edf8e6 --- /dev/null +++ b/app/exov2/exov2.xcodeproj/xcuserdata/samikhan.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + exov2.xcscheme_^#shared#^_ + + orderHint + 0 + + + SuppressBuildableAutocreation + + E07D64B92E36127E009BFB4D + + primary + + + E07D64CA2E36127F009BFB4D + + primary + + + E07D64D42E36127F009BFB4D + + primary + + + + + diff --git a/app/exov2/exov2/Preview Content/Preview Assets.xcassets/Contents.json b/app/exov2/exov2/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/exov2/exov2/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/exov2/exov2/ProcessManager.swift b/app/exov2/exov2/ProcessManager.swift new file mode 100644 index 00000000..81c5275a --- /dev/null +++ b/app/exov2/exov2/ProcessManager.swift @@ -0,0 +1,377 @@ +import Foundation +import OSLog +import SwiftUI +import AppKit +import ServiceManagement + +extension NSApplication { + func addTerminationHandler(_ handler: @escaping () -> Void) { + NSApp.setActivationPolicy(.accessory) + NotificationCenter.default.addObserver(forName: NSApplication.willTerminateNotification, + object: nil, + queue: .main) { _ in + handler() + } + } +} + +class ProcessManager: ObservableObject { + @Published var masterProcess: Process? + @Published var workerProcess: Process? + @Published var masterStatus: String = "Stopped" + @Published var workerStatus: String = "Stopped" + @Published var isLoginItemEnabled: Bool = false + @Published var isMasterMode: Bool = false // Default to replica mode (false) + + private var masterStdout: Pipe? + private var workerStdout: Pipe? + private let logger = Logger(subsystem: "exolabs.exov2", category: "ProcessManager") + + // Add file handle properties to track them + private var masterFileHandle: FileHandle? + private var workerFileHandle: FileHandle? + + private let loginService = SMAppService.mainApp + + // Find uv executable in common installation paths + private var uvPath: String? { + let commonPaths = [ + "/usr/local/bin/uv", + "/opt/homebrew/bin/uv", + "/usr/bin/uv", + "/bin/uv", + "/Users/\(NSUserName())/.cargo/bin/uv", + "/Users/\(NSUserName())/.local/bin/uv" + ] + + for path in commonPaths { + if FileManager.default.fileExists(atPath: path) { + return path + } + } + + // Try using 'which uv' command as fallback + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/which") + process.arguments = ["uv"] + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = Pipe() + + do { + try process.run() + process.waitUntilExit() + + if process.terminationStatus == 0 { + let data = pipe.fileHandleForReading.readDataToEndOfFile() + if let path = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), + !path.isEmpty { + return path + } + } + } catch { + logger.error("Failed to run 'which uv': \(error.localizedDescription)") + } + + return nil + } + + // Project root path - assuming the app bundle is in the project directory + private var projectPath: URL? { + // Get the app bundle path and navigate to the project root + // This assumes the app is built/run from within the project directory + guard let bundlePath = Bundle.main.bundleURL.path as String? else { return nil } + + // Navigate up from the app bundle to find the project root + // Look for pyproject.toml to identify the project root + var currentPath = URL(fileURLWithPath: bundlePath) + while currentPath.pathComponents.count > 1 { + let pyprojectPath = currentPath.appendingPathComponent("pyproject.toml") + if FileManager.default.fileExists(atPath: pyprojectPath.path) { + return currentPath + } + currentPath = currentPath.deletingLastPathComponent() + } + + // Fallback: try to find project in common development locations + let homeDir = FileManager.default.homeDirectoryForCurrentUser + let commonPaths = [ + "exo", + "Projects/exo", + "Documents/exo", + "Desktop/exo" + ] + + for path in commonPaths { + let projectDir = homeDir.appendingPathComponent(path) + let pyprojectPath = projectDir.appendingPathComponent("pyproject.toml") + if FileManager.default.fileExists(atPath: pyprojectPath.path) { + return projectDir + } + } + + return nil + } + + init() { + // Add termination handler + NSApplication.shared.addTerminationHandler { [weak self] in + self?.stopAll() + } + + // Check if login item is enabled + isLoginItemEnabled = (loginService.status == .enabled) + + // Start processes automatically + startMaster() + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.startWorker() + } + } + + private func handleProcessOutput(_ pipe: Pipe, processName: String) -> FileHandle { + let fileHandle = pipe.fileHandleForReading + fileHandle.readabilityHandler = { [weak self] handle in + guard let data = try? handle.read(upToCount: 1024), + let output = String(data: data, encoding: .utf8) else { + return + } + + DispatchQueue.main.async { + self?.logger.info("\(processName) output: \(output)") + print("[\(processName)] \(output)") + } + } + return fileHandle + } + + private func cleanupProcess(process: Process?, fileHandle: FileHandle?, pipe: Pipe?) { + // Remove readability handler + fileHandle?.readabilityHandler = nil + + // Close file handles + try? fileHandle?.close() + try? pipe?.fileHandleForReading.close() + try? pipe?.fileHandleForWriting.close() + + // Terminate process if still running + if process?.isRunning == true { + process?.terminate() + } + } + + func startMaster() { + guard let projectPath = self.projectPath else { + masterStatus = "Error: Project directory not found" + logger.error("Could not find project directory with pyproject.toml") + return + } + + guard let uvPath = self.uvPath else { + masterStatus = "Error: uv not found" + logger.error("Could not find uv executable in common paths") + return + } + + // Cleanup any existing process + cleanupProcess(process: masterProcess, fileHandle: masterFileHandle, pipe: masterStdout) + + masterProcess = Process() + masterStdout = Pipe() + + // Use uv to run the master module + masterProcess?.executableURL = URL(fileURLWithPath: uvPath) + masterProcess?.arguments = ["run", "python", "-m", "master.main"] + masterProcess?.standardOutput = masterStdout + masterProcess?.standardError = masterStdout + + // Set up environment + var env = ProcessInfo.processInfo.environment + env["PYTHONUNBUFFERED"] = "1" + env["PYTHONPATH"] = projectPath.path + + // Set replica mode if not in master mode + if !self.isMasterMode { + env["EXO_RUN_AS_REPLICA"] = "1" + } + + masterProcess?.environment = env + + // Set working directory to project root + masterProcess?.currentDirectoryURL = projectPath + + // Store the file handle + masterFileHandle = handleProcessOutput(masterStdout!, processName: "Master") + + do { + logger.info("Starting master process with \(uvPath) run python -m master.main at \(projectPath.path)") + try masterProcess?.run() + masterStatus = "Running" + + masterProcess?.terminationHandler = { [weak self] process in + DispatchQueue.main.async { + let status = "Stopped (exit: \(process.terminationStatus))" + self?.masterStatus = status + self?.logger.error("Master process terminated: \(status)") + // Cleanup on termination + self?.cleanupProcess(process: self?.masterProcess, + fileHandle: self?.masterFileHandle, + pipe: self?.masterStdout) + } + } + } catch { + masterStatus = "Error: \(error.localizedDescription)" + logger.error("Failed to start master: \(error.localizedDescription)") + cleanupProcess(process: masterProcess, fileHandle: masterFileHandle, pipe: masterStdout) + } + } + + func startWorker() { + guard let projectPath = self.projectPath else { + workerStatus = "Error: Project directory not found" + logger.error("Could not find project directory with pyproject.toml") + return + } + + guard let uvPath = self.uvPath else { + workerStatus = "Error: uv not found" + logger.error("Could not find uv executable in common paths") + return + } + + // Cleanup any existing process + cleanupProcess(process: workerProcess, fileHandle: workerFileHandle, pipe: workerStdout) + + workerProcess = Process() + workerStdout = Pipe() + + // Use uv to run the worker module + workerProcess?.executableURL = URL(fileURLWithPath: uvPath) + workerProcess?.arguments = ["run", "python", "-m", "worker.main"] + workerProcess?.standardOutput = workerStdout + workerProcess?.standardError = workerStdout + + // Set up environment + var env = ProcessInfo.processInfo.environment + env["PYTHONUNBUFFERED"] = "1" + env["PYTHONPATH"] = projectPath.path + workerProcess?.environment = env + + // Set working directory to project root + workerProcess?.currentDirectoryURL = projectPath + + // Store the file handle + workerFileHandle = handleProcessOutput(workerStdout!, processName: "Worker") + + do { + logger.info("Starting worker process with \(uvPath) run python -m worker.main at \(projectPath.path)") + try workerProcess?.run() + workerStatus = "Running" + + workerProcess?.terminationHandler = { [weak self] process in + DispatchQueue.main.async { + let status = "Stopped (exit: \(process.terminationStatus))" + self?.workerStatus = status + self?.logger.error("Worker process terminated: \(status)") + // Cleanup on termination + self?.cleanupProcess(process: self?.workerProcess, + fileHandle: self?.workerFileHandle, + pipe: self?.workerStdout) + } + } + } catch { + workerStatus = "Error: \(error.localizedDescription)" + logger.error("Failed to start worker: \(error.localizedDescription)") + cleanupProcess(process: workerProcess, fileHandle: workerFileHandle, pipe: workerStdout) + } + } + + func stopAll() { + logger.info("Stopping all processes") + + // Clean up master process + cleanupProcess(process: masterProcess, fileHandle: masterFileHandle, pipe: masterStdout) + masterProcess = nil + masterStdout = nil + masterFileHandle = nil + masterStatus = "Stopped" + + // Clean up worker process + cleanupProcess(process: workerProcess, fileHandle: workerFileHandle, pipe: workerStdout) + workerProcess = nil + workerStdout = nil + workerFileHandle = nil + workerStatus = "Stopped" + } + + func checkBinaries() -> Bool { + guard let projectPath = self.projectPath else { + logger.error("Could not find project directory") + return false + } + + guard let uvPath = self.uvPath else { + logger.error("Could not find uv executable") + return false + } + + let fileManager = FileManager.default + let pyprojectPath = projectPath.appendingPathComponent("pyproject.toml").path + let masterPath = projectPath.appendingPathComponent("master/main.py").path + let workerPath = projectPath.appendingPathComponent("worker/main.py").path + + let uvExists = fileManager.fileExists(atPath: uvPath) + let pyprojectExists = fileManager.fileExists(atPath: pyprojectPath) + let masterExists = fileManager.fileExists(atPath: masterPath) + let workerExists = fileManager.fileExists(atPath: workerPath) + + if !uvExists { + logger.error("uv not found at \(uvPath)") + } + if !pyprojectExists { + logger.error("pyproject.toml not found at \(pyprojectPath)") + } + if !masterExists { + logger.error("master/main.py not found at \(masterPath)") + } + if !workerExists { + logger.error("worker/main.py not found at \(workerPath)") + } + + return uvExists && pyprojectExists && masterExists && workerExists + } + + func toggleLoginItem() { + do { + if isLoginItemEnabled { + try loginService.unregister() + } else { + try loginService.register() + } + isLoginItemEnabled = (loginService.status == .enabled) + } catch { + logger.error("Failed to toggle login item: \(error.localizedDescription)") + } + } + + func toggleMasterMode() { + isMasterMode.toggle() + logger.info("Toggling master mode to: \(self.isMasterMode ? "Master" : "Replica")") + + // Restart master process with new mode + if masterProcess?.isRunning == true { + // Clean up current master process + cleanupProcess(process: masterProcess, fileHandle: masterFileHandle, pipe: masterStdout) + masterProcess = nil + masterStdout = nil + masterFileHandle = nil + masterStatus = "Stopped" + + // Start master with new mode after a brief delay + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.startMaster() + } + } + } +} \ No newline at end of file diff --git a/app/exov2/exov2/exov2.entitlements b/app/exov2/exov2/exov2.entitlements new file mode 100644 index 00000000..9b5d06d4 --- /dev/null +++ b/app/exov2/exov2/exov2.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.automation.apple-events + + + diff --git a/app/exov2/exov2/exov2App.swift b/app/exov2/exov2/exov2App.swift new file mode 100644 index 00000000..2a6910b6 --- /dev/null +++ b/app/exov2/exov2/exov2App.swift @@ -0,0 +1,115 @@ +// +// exov2App.swift +// exov2 +// +// Created by Sami Khan on 2025-07-27. +// + +import SwiftUI +import AppKit +import Foundation +import OSLog +import ServiceManagement + +@main +struct exov2App: App { + @StateObject private var processManager = ProcessManager() + + private func resizedMenuBarIcon(named: String, size: CGFloat = 18.0) -> NSImage? { + guard let original = NSImage(named: named) else { + print("Failed to load image named: \(named)") + return nil + } + + let resized = NSImage(size: NSSize(width: size, height: size), flipped: false) { rect in + NSGraphicsContext.current?.imageInterpolation = .high + original.draw(in: rect) + return true + } + + resized.isTemplate = false + resized.size = NSSize(width: size, height: size) + return resized + } + + var body: some Scene { + MenuBarExtra { + MenuBarView(processManager: processManager) + } label: { + if let resizedImage = resizedMenuBarIcon(named: "menubar-icon") { + Image(nsImage: resizedImage) + .opacity(processManager.masterStatus == "Running" ? 1.0 : 0.5) + } + } + .menuBarExtraStyle(.window) + } +} + +struct MenuBarView: View { + @ObservedObject var processManager: ProcessManager + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + StatusSection(processManager: processManager) + + Divider() + + Toggle("Launch at Login", isOn: Binding( + get: { processManager.isLoginItemEnabled }, + set: { _ in processManager.toggleLoginItem() } + )) + .padding(.horizontal) + + Toggle("Is Master?", isOn: Binding( + get: { processManager.isMasterMode }, + set: { _ in processManager.toggleMasterMode() } + )) + .padding(.horizontal) + + Divider() + + Button("Quit") { + NSApplication.shared.terminate(nil) + } + } + .padding() + .frame(width: 250) + .onAppear { + if !processManager.checkBinaries() { + showEnvironmentError() + } + } + } + + private func showEnvironmentError() { + let alert = NSAlert() + alert.messageText = "Python Environment Error" + alert.informativeText = "Could not find the required Python environment, uv, or project files. Please ensure uv is installed and the project directory is accessible." + alert.alertStyle = .critical + alert.addButton(withTitle: "OK") + alert.runModal() + NSApplication.shared.terminate(nil) + } +} + +struct StatusSection: View { + @ObservedObject var processManager: ProcessManager + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text("Master:") + .bold() + Text(processManager.masterStatus) + .foregroundColor(processManager.masterStatus == "Running" ? .green : .red) + } + + HStack { + Text("Worker:") + .bold() + Text(processManager.workerStatus) + .foregroundColor(processManager.workerStatus == "Running" ? .green : .red) + } + } + } +} diff --git a/app/exov2/exov2Tests/exov2Tests.swift b/app/exov2/exov2Tests/exov2Tests.swift new file mode 100644 index 00000000..dd137fbd --- /dev/null +++ b/app/exov2/exov2Tests/exov2Tests.swift @@ -0,0 +1,17 @@ +// +// exov2Tests.swift +// exov2Tests +// +// Created by Sami Khan on 2025-07-27. +// + +import Testing +@testable import exov2 + +struct exov2Tests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/app/exov2/exov2UITests/exov2UITests.swift b/app/exov2/exov2UITests/exov2UITests.swift new file mode 100644 index 00000000..db1586a9 --- /dev/null +++ b/app/exov2/exov2UITests/exov2UITests.swift @@ -0,0 +1,43 @@ +// +// exov2UITests.swift +// exov2UITests +// +// Created by Sami Khan on 2025-07-27. +// + +import XCTest + +final class exov2UITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/app/exov2/exov2UITests/exov2UITestsLaunchTests.swift b/app/exov2/exov2UITests/exov2UITestsLaunchTests.swift new file mode 100644 index 00000000..928b4443 --- /dev/null +++ b/app/exov2/exov2UITests/exov2UITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// exov2UITestsLaunchTests.swift +// exov2UITests +// +// Created by Sami Khan on 2025-07-27. +// + +import XCTest + +final class exov2UITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}