diff --git a/.github/actions/setup-odrcore/action.yml b/.github/actions/setup-odrcore/action.yml deleted file mode 100644 index 9cb0544..0000000 --- a/.github/actions/setup-odrcore/action.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: setup odrcore -description: > - Installs the toolchain and resolves odrcore and its dependencies with conan, - leaving the generated xcconfigs in conan-output and the runtime assets in - conan-assets. Every job that compiles the app needs the exact same ten steps, - so they live here instead of being copied into each workflow. - -inputs: - xcode-version: - description: Xcode version to select on the runner - required: true - profile: - description: conan host profile under conan/profiles - required: true - configuration: - description: Xcode configuration to resolve the dependencies for - required: true - cache-flavor: - description: > - Cache namespace. The simulator and the device slice resolve a different - dependency set, so they must not overwrite each other's cache entry. - required: true - save-cache: - description: > - Whether this job may write the conan cache back. Jobs that only consume - what another job built restore without saving. - required: false - default: "true" - -outputs: - conan-cache-key: - description: Cache key of the conan package cache, to restore it elsewhere - value: ${{ steps.conan-cache-key.outputs.key }} - -runs: - using: composite - steps: - - name: checkout conan-odr-index - shell: bash - run: git submodule update --init --depth 1 conan-odr-index - - # 3.12+ is required by the conan-odr-index helper scripts - - uses: actions/setup-python@v7 - with: - python-version: "3.12" - - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ inputs.xcode-version }} - - # conan itself is pinned by conan-odr-index so recipes and client stay in sync - - name: install conan - shell: bash - run: pip3 install -r conan-odr-index/requirements.txt - - - name: conan profile - shell: bash - run: conan profile detect - - - name: conan cache key - id: conan-cache-key - shell: bash - run: echo "key=conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}-index$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conan/conanfile.py', 'conan/profiles/*') }}" >> "$GITHUB_OUTPUT" - - # restore before exporting the recipes: the export must run on top of the - # restored cache so current recipes win over stale ones from the archive - - name: conan cache - if: inputs.save-cache == 'true' - uses: actions/cache@v6 - with: - path: ~/.conan2/p - key: ${{ steps.conan-cache-key.outputs.key }} - restore-keys: | - conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}- - - - name: restore conan cache - if: inputs.save-cache != 'true' - uses: actions/cache/restore@v6 - with: - path: ~/.conan2/p - key: ${{ steps.conan-cache-key.outputs.key }} - restore-keys: | - conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}- - - # recipes are exported from the submodule into the local cache, which is - # why no private remote has to be configured - - name: export conan-odr-index - shell: bash - run: python3 conan-odr-index/scripts/conan_export_all_packages.py - - - name: conan install - shell: bash - run: > - conan install conan/ - --output-folder=conan-output - --build=missing - --profile:host=conan/profiles/${{ inputs.profile }} - --deployer=conan/conandeployer.py - --deployer-folder=conan-assets - -o "configuration=${{ inputs.configuration }}" diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index a70080c..c83a90f 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -38,20 +38,13 @@ jobs: with: bundler-cache: true - - uses: ./.github/actions/setup-odrcore + - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.xcode_version }} - profile: ios-simulator-arm64 - configuration: Debug - cache-flavor: simulator - name: run tests run: bundle exec fastlane tests - # drop temporary download/source/build folders so only built packages get cached - - name: clean conan cache - run: conan cache clean "*" - - uses: actions/upload-artifact@v7 if: always() with: @@ -73,12 +66,9 @@ jobs: - name: checkout uses: actions/checkout@v7 - - uses: ./.github/actions/setup-odrcore + - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.xcode_version }} - profile: ios - configuration: ${{ matrix.configuration }} - cache-flavor: device # signing needs secrets this workflow does not have, so this only proves # that the device slice compiles and links @@ -92,6 +82,3 @@ jobs: -destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO build - - - name: clean conan cache - run: conan cache clean "*" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f93eb21..28e87a2 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,7 +1,7 @@ name: format -# swift-format comes with the Xcode toolchain, so this needs neither conan nor -# ruby and reports style breakage in a minute instead of after a full build. +# swift-format comes with the Xcode toolchain, so this needs no ruby and reports +# style breakage in a minute instead of after a full build. # Unlike build_test this has no paths-ignore: every file the formatter touches # gets checked. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85baca9..7342f43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,15 +57,9 @@ jobs: with: bundler-cache: true - # a release consumes what build_test already built and never writes the - # cache back, so an untested release cannot poison it - - uses: ./.github/actions/setup-odrcore + - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.xcode_version }} - profile: ios - configuration: ${{ matrix.flavor == 'pro' && 'Release' || 'Release Lite' }} - cache-flavor: device - save-cache: "false" # a throwaway keychain so the distribution certificate never outlives the job - name: import signing certificate diff --git a/.gitignore b/.gitignore index cd06563..5d336b6 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,5 @@ fastlane/test_output fastlane/report.xml -conan-output/ -conan-assets/ graph_info.json .venv/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 97d2b5b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "conan-odr-index"] - path = conan-odr-index - url = https://github.com/opendocument-app/conan-odr-index.git diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index 19e1d86..5b4e585 100644 --- a/OpenDocumentReader.xcodeproj/project.pbxproj +++ b/OpenDocumentReader.xcodeproj/project.pbxproj @@ -21,29 +21,30 @@ ACA46529244E403900A5DA7A /* AppType.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACA46528244E403900A5DA7A /* AppType.swift */; }; ACD9BE3C2444A371009014E6 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD9BE2D2444A371009014E6 /* ConfigurationManager.swift */; }; BFFB694BE9E3E744FD4F92E0 /* CrashManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25ACB597DDB073F3AFC74345 /* CrashManager.swift */; }; + C43B65A97E4029F7790DA729 /* CoreWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C00CD0A89EF1F64FF879A1 /* CoreWrapper.swift */; }; D2DCD7104EBCC0F1A784E116 /* GoogleMobileAds in Frameworks */ = {isa = PBXBuildFile; productRef = B5F033C5AA89A6E9C19EA27D /* GoogleMobileAds */; }; E1A78CF72C1A53DB00CD43E4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1EB6C492C1A510D003EC5A0 /* Foundation.framework */; }; E2064DFE22CFA1BA006441F8 /* iAd.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2064DDF22CFA1BA006441F8 /* iAd.framework */; }; E22B252F2557F0E2001D0C52 /* OpenDocumentReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22B252E2557F0E2001D0C52 /* OpenDocumentReaderTests.swift */; }; - E2A17B0400000000000000A4 /* PageTabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0300000000000000A3 /* PageTabBarTests.swift */; }; E22EB716226B621200053B86 /* DocumentBrowserTransitioningDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22EB6F7226B621200053B86 /* DocumentBrowserTransitioningDelegate.swift */; }; E22EB718226B633500053B86 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22EB717226B633500053B86 /* Document.swift */; }; E22EB71A226B65E300053B86 /* DocumentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22EB719226B65E300053B86 /* DocumentViewController.swift */; }; - E2A17B0200000000000000A2 /* PageTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0100000000000000A1 /* PageTabBar.swift */; }; E22EB71C226B66B300053B86 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E22EB71B226B66B300053B86 /* Main.storyboard */; }; E23795302274844400BA7238 /* AdSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E237952F2274844400BA7238 /* AdSupport.framework */; }; E24110312586349500800247 /* test.odt in Resources */ = {isa = PBXBuildFile; fileRef = E24110232586349500800247 /* test.odt */; }; + E26C39392250DC6E009C484A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E26C39382250DC6E009C484A /* WebKit.framework */; }; + E2A17B0200000000000000A2 /* PageTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0100000000000000A1 /* PageTabBar.swift */; }; + E2A17B0400000000000000A4 /* PageTabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0300000000000000A3 /* PageTabBarTests.swift */; }; E2A17B1000000000000000B0 /* test.ods in Resources */ = {isa = PBXBuildFile; fileRef = E2A17B1200000000000000B2 /* test.ods */; }; E2A17B1100000000000000B1 /* test.odp in Resources */ = {isa = PBXBuildFile; fileRef = E2A17B1300000000000000B3 /* test.odp */; }; - E26C39392250DC6E009C484A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E26C39382250DC6E009C484A /* WebKit.framework */; }; E2A17B2000000000000000C0 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A17B2200000000000000C2 /* CoreFoundation.framework */; }; E2A17B2100000000000000C1 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A17B2300000000000000C3 /* CFNetwork.framework */; }; - E2C008FA220F1CF80097C594 /* CoreWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = E2C008F9220F1CF80097C594 /* CoreWrapper.mm */; }; E2D0B3D9226D945400534FCC /* StoreReviewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D0B3D8226D945400534FCC /* StoreReviewHelper.swift */; }; E2F7ED53220B54D600D63515 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F7ED52220B54D600D63515 /* AppDelegate.swift */; }; E2F7ED55220B54D600D63515 /* DocumentBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F7ED54220B54D600D63515 /* DocumentBrowserViewController.swift */; }; E2F7ED5E220B54D700D63515 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2F7ED5D220B54D700D63515 /* Assets.xcassets */; }; E2F7ED61220B54D700D63515 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E2F7ED5F220B54D700D63515 /* LaunchScreen.storyboard */; }; + FB494C85E264BD924C1EA54E /* OdrCore in Frameworks */ = {isa = PBXBuildFile; productRef = E381918B979121CD165C966A /* OdrCore */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,6 +58,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02C00CD0A89EF1F64FF879A1 /* CoreWrapper.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CoreWrapper.swift; path = CoreWrapper.swift; sourceTree = ""; }; 25ACB597DDB073F3AFC74345 /* CrashManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrashManager.swift; sourceTree = ""; }; 2DF2708F60BE3388B124CABF /* AnalyticsManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnalyticsManager.swift; sourceTree = ""; }; 523A371228CCF27400876C77 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = System/Library/Frameworks/AdServices.framework; sourceTree = SDKROOT; }; @@ -89,27 +91,23 @@ B01C1B2BA00A7917FF7D7726 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; B94897965C815527C6C06996 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; C4579E708F099784EF583B35 /* SceneDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - D14280D860FAA1DD046BCB5C /* conan.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = conan.xcconfig; sourceTree = ""; }; E1EB6C492C1A510D003EC5A0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; E2064DDF22CFA1BA006441F8 /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = System/Library/Frameworks/iAd.framework; sourceTree = SDKROOT; }; E22B252E2557F0E2001D0C52 /* OpenDocumentReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenDocumentReaderTests.swift; sourceTree = ""; }; - E2A17B0300000000000000A3 /* PageTabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBarTests.swift; sourceTree = ""; }; E22B25302557F0E2001D0C52 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E22EB6F7226B621200053B86 /* DocumentBrowserTransitioningDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentBrowserTransitioningDelegate.swift; sourceTree = ""; }; E22EB717226B633500053B86 /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; E22EB719226B65E300053B86 /* DocumentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentViewController.swift; sourceTree = ""; }; - E2A17B0100000000000000A1 /* PageTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBar.swift; sourceTree = ""; }; E22EB71B226B66B300053B86 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; E237952F2274844400BA7238 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; E24110232586349500800247 /* test.odt */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.odt; sourceTree = ""; }; + E26C39382250DC6E009C484A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + E2A17B0100000000000000A1 /* PageTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBar.swift; sourceTree = ""; }; + E2A17B0300000000000000A3 /* PageTabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBarTests.swift; sourceTree = ""; }; E2A17B1200000000000000B2 /* test.ods */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.ods; sourceTree = ""; }; E2A17B1300000000000000B3 /* test.odp */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.odp; sourceTree = ""; }; - E26C39382250DC6E009C484A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; E2A17B2200000000000000C2 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; E2A17B2300000000000000C3 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; - E2BB4B60220EF3A10056176B /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; - E2C008F9220F1CF80097C594 /* CoreWrapper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CoreWrapper.mm; sourceTree = ""; }; - E2C008FC220F1D570097C594 /* CoreWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreWrapper.h; sourceTree = ""; }; E2D0B3D8226D945400534FCC /* StoreReviewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreReviewHelper.swift; sourceTree = ""; }; E2F7ED52220B54D600D63515 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E2F7ED54220B54D600D63515 /* DocumentBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentBrowserViewController.swift; sourceTree = ""; }; @@ -140,6 +138,7 @@ 523A371328CCF27400876C77 /* AdServices.framework in Frameworks */, E1A78CF72C1A53DB00CD43E4 /* Foundation.framework in Frameworks */, D2DCD7104EBCC0F1A784E116 /* GoogleMobileAds in Frameworks */, + FB494C85E264BD924C1EA54E /* OdrCore in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -187,7 +186,6 @@ E17AF34D2C258C5100FBED6A /* configs */ = { isa = PBXGroup; children = ( - D14280D860FAA1DD046BCB5C /* conan.xcconfig */, ); path = configs; sourceTree = ""; @@ -252,14 +250,12 @@ E2F7ED5F220B54D700D63515 /* LaunchScreen.storyboard */, E2F7ED62220B54D700D63515 /* Info.plist */, ACF1A3E42469F8DE000BA420 /* Info-Lite.plist */, - E2BB4B60220EF3A10056176B /* BridgingHeader.h */, - E2C008F9220F1CF80097C594 /* CoreWrapper.mm */, - E2C008FC220F1D570097C594 /* CoreWrapper.h */, E2D0B3D8226D945400534FCC /* StoreReviewHelper.swift */, AC125F182435311A008AD515 /* Localizable.strings */, 3FF7388D538126B646A5ACC6 /* Privacy */, 9113B590877803CA250DDFB7 /* NonFree */, C4579E708F099784EF583B35 /* SceneDelegate.swift */, + 02C00CD0A89EF1F64FF879A1 /* CoreWrapper.swift */, ); path = OpenDocumentReader; sourceTree = ""; @@ -292,7 +288,6 @@ E2F7ED4B220B54D600D63515 /* Sources */, E2F7ED4C220B54D600D63515 /* Frameworks */, E2F7ED4D220B54D600D63515 /* Resources */, - E158977C2DF640E700319830 /* Copy Conan Resources */, ); buildRules = ( ); @@ -301,6 +296,7 @@ name = OpenDocumentReader; packageProductDependencies = ( B5F033C5AA89A6E9C19EA27D /* GoogleMobileAds */, + E381918B979121CD165C966A /* OdrCore */, ); productName = "OpenDocument Reader"; productReference = 52A348B12A9262D100DACAB9 /* OpenDocumentReader.app */; @@ -353,6 +349,7 @@ mainGroup = E2F7ED46220B54D600D63515; packageReferences = ( AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */, + 78200B97C10509E2539B4FE5 /* XCRemoteSwiftPackageReference "OpenDocument.core" */, ); productRefGroup = E2F7ED46220B54D600D63515; projectDirPath = ""; @@ -390,29 +387,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - E158977C2DF640E700319830 /* Copy Conan Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PROJECT_DIR}/conan-assets/input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "Copy Conan Resources"; - outputFileListPaths = ( - "${PROJECT_DIR}/conan-assets/output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PROJECT_DIR}/conan/copy-resources.sh\"\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ E22B25282557F0E2001D0C52 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -432,7 +406,6 @@ AC384BCD23B4FFA700C7BF47 /* ContentViewController.swift in Sources */, E22EB718226B633500053B86 /* Document.swift in Sources */, AC384BCF23B4FFA700C7BF47 /* PageViewController.swift in Sources */, - E2C008FA220F1CF80097C594 /* CoreWrapper.mm in Sources */, E2D0B3D9226D945400534FCC /* StoreReviewHelper.swift in Sources */, E22EB716226B621200053B86 /* DocumentBrowserTransitioningDelegate.swift in Sources */, ACA46529244E403900A5DA7A /* AppType.swift in Sources */, @@ -443,6 +416,7 @@ 32B6BA42C6D6D443ED2A9697 /* AnalyticsManager.swift in Sources */, BFFB694BE9E3E744FD4F92E0 /* CrashManager.swift in Sources */, 229BA8DA70F3CA88BA547FA9 /* SceneDelegate.swift in Sources */, + C43B65A97E4029F7790DA729 /* CoreWrapper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -557,7 +531,6 @@ }; AC8468CC244E255400E96707 /* Debug Lite */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D14280D860FAA1DD046BCB5C /* conan.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = "OpenDocumentReader Lite"; @@ -568,7 +541,6 @@ CURRENT_PROJECT_VERSION = 42; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_ASSET_PATHS = "conan-assets"; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -587,7 +559,6 @@ PRODUCT_NAME = "$(BUNDLE_DISPLAY_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = OpenDocumentReader/BridgingHeader.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -654,7 +625,6 @@ }; AC8468D0244E256900E96707 /* Release Lite */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D14280D860FAA1DD046BCB5C /* conan.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = "OpenDocumentReader Lite"; @@ -664,7 +634,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 42; DEFINES_MODULE = YES; - DEVELOPMENT_ASSET_PATHS = "conan-assets"; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -682,7 +651,6 @@ PRODUCT_NAME = "$(BUNDLE_DISPLAY_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = OpenDocumentReader/BridgingHeader.h; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -890,7 +858,6 @@ }; E2F7ED66220B54D700D63515 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D14280D860FAA1DD046BCB5C /* conan.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = OpenDocumentReader; @@ -901,7 +868,6 @@ CURRENT_PROJECT_VERSION = 42; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_ASSET_PATHS = "conan-assets"; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -920,7 +886,6 @@ PRODUCT_NAME = "$(BUNDLE_DISPLAY_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = OpenDocumentReader/BridgingHeader.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -929,7 +894,6 @@ }; E2F7ED67220B54D700D63515 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D14280D860FAA1DD046BCB5C /* conan.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = OpenDocumentReader; @@ -939,7 +903,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 42; DEFINES_MODULE = YES; - DEVELOPMENT_ASSET_PATHS = "conan-assets"; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -957,7 +920,6 @@ PRODUCT_NAME = "$(BUNDLE_DISPLAY_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = OpenDocumentReader/BridgingHeader.h; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1002,6 +964,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 78200B97C10509E2539B4FE5 /* XCRemoteSwiftPackageReference "OpenDocument.core" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/opendocument-app/OpenDocument.core.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.2.0; + }; + }; AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/googleads/swift-package-manager-google-mobile-ads.git"; @@ -1018,6 +988,11 @@ package = AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */; productName = GoogleMobileAds; }; + E381918B979121CD165C966A /* OdrCore */ = { + isa = XCSwiftPackageProductDependency; + package = 78200B97C10509E2539B4FE5 /* XCRemoteSwiftPackageReference "OpenDocument.core" */; + productName = OdrCore; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = E2F7ED47220B54D600D63515 /* Project object */; diff --git a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e6b95c3..121caff 100644 --- a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "3b9752ff554d5fdc91a7c01c2322a5d851b0979c36f02eb55188d9ab9e3d7e77", + "originHash" : "fb74b10a80d07407e1cef4ff366c543de15a310f0a4ae23e7fdda72de7a12f43", "pins" : [ + { + "identity" : "opendocument.core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/opendocument-app/OpenDocument.core.git", + "state" : { + "revision" : "72661abacff00bbcafbc142a3655f1bd075e95df", + "version" : "6.2.0" + } + }, { "identity" : "swift-package-manager-google-mobile-ads", "kind" : "remoteSourceControl", diff --git a/OpenDocumentReader/BridgingHeader.h b/OpenDocumentReader/BridgingHeader.h deleted file mode 100644 index e502096..0000000 --- a/OpenDocumentReader/BridgingHeader.h +++ /dev/null @@ -1,5 +0,0 @@ -// -// Use this file to import your target's public headers that you would like to expose to Swift. -// - -#include "CoreWrapper.h" diff --git a/OpenDocumentReader/CoreWrapper.h b/OpenDocumentReader/CoreWrapper.h deleted file mode 100644 index 3bcf757..0000000 --- a/OpenDocumentReader/CoreWrapper.h +++ /dev/null @@ -1,55 +0,0 @@ -// -// CoreWrapper.h -// OpenDocument Reader -// -// Created by Thomas Taschauer on 09.02.19. -// Copyright © 2019 Thomas Taschauer. All rights reserved. -// - -#ifndef CoreWrapper_h -#define CoreWrapper_h - -#import - -NS_ASSUME_NONNULL_BEGIN - -extern NSErrorDomain const CoreWrapperErrorDomain; - -typedef NS_ERROR_ENUM(CoreWrapperErrorDomain, CoreWrapperError) { - /// odrcore threw something we have no specific handling for. - CoreWrapperErrorUnknown = 1, - /// The document is encrypted and the supplied password did not open it. - CoreWrapperErrorWrongPassword = 2, - /// Not a document odrcore can translate. PDFs land here on purpose. - CoreWrapperErrorUnsupportedFileType = 3, -}; - -@interface CoreWrapper : NSObject - -@property (nonatomic, copy, readonly) NSArray *pageNames; - -/// One URL per page, in the order they should be shown. Loopback HTTP URLs -/// served by odrcore, or `file:` URLs below the output path when the app is -/// built to render offline - see `kCoreWrapperServesOverHttp`. -@property (nonatomic, copy, readonly) NSArray *pageURLs; - -- (BOOL)translate:(NSString *)inputPath - cache:(NSString *)cachePath - into:(NSString *)outputPath - with:(nullable NSString *)password - editable:(BOOL)editable - error:(NSError **)error; - -- (BOOL)backTranslate:(NSString *)diff - into:(NSString *)outputPath - error:(NSError **)error; - -/// Whether this URL is one odrcore is serving, rather than somewhere a link in -/// the document leads. -+ (BOOL)isServedURL:(NSURL *)url; - -@end - -NS_ASSUME_NONNULL_END - -#endif /* CoreWrapper_h */ diff --git a/OpenDocumentReader/CoreWrapper.mm b/OpenDocumentReader/CoreWrapper.mm deleted file mode 100644 index c989c23..0000000 --- a/OpenDocumentReader/CoreWrapper.mm +++ /dev/null @@ -1,345 +0,0 @@ -// -// CoreWrapper.mm -// OpenDocument Reader -// -// Created by Thomas Taschauer on 09.02.19. -// Copyright © 2019 Thomas Taschauer. All rights reserved. -// - -#import - -#import "CoreWrapper.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -/// Whether the translated HTML reaches the web view over loopback HTTP instead -/// of being written to the output directory as files. -/// -/// HTTP is what OpenDocument.droid does and where this is headed. odrcore then -/// renders a page when the web view asks for it, on one of the server's -/// threads, rather than rendering every page up front on whichever thread -/// called `translate`. Set this to `NO` to go back to files - the offline path -/// is kept working until the migration is finished, not as a runtime option. -static const BOOL kCoreWrapperServesOverHttp = YES; - -/// Loopback address for that server. Nothing off the device can reach it. -static NSString *const kCoreWrapperHttpHost = @"127.0.0.1"; - -NSErrorDomain const CoreWrapperErrorDomain = @"app.opendocument.CoreWrapperErrorDomain"; - -static NSError *CoreWrapperMakeError(CoreWrapperError code, NSString *description) { - return [NSError errorWithDomain:CoreWrapperErrorDomain - code:code - userInfo:@{NSLocalizedDescriptionKey: description}]; -} - -/// The view odrcore names "document" holds the whole file in one page: every -/// slide of a presentation, every page of a PDF, the entire text document. -static bool CoreWrapperIsCombinedView(const odr::HtmlView &view) { - return view.name() == "document"; -} - -/// Picks the views to show as pages, the same way OpenDocument.droid does. -/// -/// Spreadsheets get one tab per sheet, because scrolling through every sheet of -/// a workbook in one page is not how anyone reads a spreadsheet. Everything else -/// gets the combined view and nothing else - a presentation would otherwise show -/// its slides twice, once inside the combined view and once per slide view, and -/// a PDF would list one tab per page next to the tab that already has them all. -/// -/// Services without a combined view - plain text and images among them - keep -/// whatever views they do offer. -static odr::HtmlViews CoreWrapperSelectViews(const odr::HtmlViews &views, - odr::DocumentType documentType) { - bool isSpreadsheet = documentType == odr::DocumentType::spreadsheet; - bool hasCombinedView = std::any_of(views.begin(), views.end(), CoreWrapperIsCombinedView); - - odr::HtmlViews selected; - for (const odr::HtmlView &view : views) { - bool isCombinedView = CoreWrapperIsCombinedView(view); - bool skip = isSpreadsheet ? isCombinedView : (hasCombinedView && !isCombinedView); - if (skip) { - continue; - } - - selected.push_back(view); - } - - return selected; -} - -static std::optional g_server; - -/// The port `bind` handed us, and with it whether there is a server at all. -/// Zero until one is running. -static std::atomic g_serverPort{0}; - -/// Brings up the one server the app has, on first use, and leaves it running -/// for the rest of the process. Returns NO when the socket could not be opened, -/// in which case translate falls back to files. -/// -/// Only attempted once: retrying would fork the state of `g_server`. -static BOOL CoreWrapperStartServer() { - static BOOL running = NO; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - try { - g_server.emplace(); - // port 0 asks for whichever port is free and bind() reports back - // which one that was. Nothing outside this process needs to know it - // in advance, and a fixed port would be one the other flavor of the - // app, or anything else on the device, could already be holding - g_serverPort.store(g_server->bind([kCoreWrapperHttpHost UTF8String], 0)); - } catch (...) { - g_server.reset(); - - return; - } - - // the socket is bound and taking connections into its backlog from here - // on, so there is nothing to wait for before handing out URLs. - // listen() only returns once the server is stopped, and the server is - // stopped when the process ends, so this thread is never joined - std::thread([] { - try { - g_server->listen(); - } catch (...) { - } - }).detach(); - - running = YES; - }); - - return running; -} - -/// Where the server serves a view: `HttpServer` routes `/file//` -/// to the service connected under that prefix, and the pages ask for their -/// images and fonts relative to that, so those are covered by the same route. -static NSURL *CoreWrapperPageURL(const std::string &prefix, const std::string &path) { - NSString *escapedPath = [[NSString stringWithUTF8String:path.c_str()] - stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]]; - - NSString *url = [NSString stringWithFormat:@"http://%@:%u/file/%s/%@", kCoreWrapperHttpHost, - static_cast(g_serverPort.load()), - prefix.c_str(), escapedPath]; - - return [NSURL URLWithString:url]; -} - -/// odrcore's data files ship inside the app bundle. The path never changes at -/// runtime, so it is set once instead of on every translate call. -static void CoreWrapperEnsureDataPath() { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; - std::string dataPath = std::string([bundlePath UTF8String]) + "/odrcore"; - odr::GlobalParams::set_odr_core_data_path(dataPath); - }); -} - -@implementation CoreWrapper { - std::optional _document; - std::optional _html; -} - -- (BOOL)translate:(NSString *)inputPath - cache:(NSString *)cachePath - into:(NSString *)outputPath - with:(NSString *)password - editable:(BOOL)editable - error:(NSError **)error { - @synchronized(self) { - _pageNames = nil; - _pageURLs = nil; - - try { - CoreWrapperEnsureDataPath(); - - _html.reset(); - - BOOL overHttp = kCoreWrapperServesOverHttp && CoreWrapperStartServer(); - - odr::HtmlConfig config; - config.editable = editable; - // resource paths are resolved relative to an output directory, and - // in server mode there is none - odrcore rejects the combination - config.relative_resource_paths = !overHttp; - - std::string inputPathCpp = std::string([inputPath UTF8String]); - - std::vector fileTypes; - try { - fileTypes = odr::list_file_types(inputPathCpp); - } catch (odr::UnsupportedFileType &) { - fileTypes.clear(); - } - if (fileTypes.empty()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnsupportedFileType, - @"odrcore does not recognise this file type"); - } - return NO; - } - - // PDFs are handed to WKWebView instead, which renders them natively - if (std::find(fileTypes.begin(), fileTypes.end(), - odr::FileType::portable_document_format) != fileTypes.end()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnsupportedFileType, - @"PDF is rendered by the web view, not by odrcore"); - } - return NO; - } - - std::string outputPathCpp = std::string([outputPath UTF8String]); - std::string cachePathCpp = std::string([cachePath UTF8String]); - - odr::DecodedFile file = odr::open(inputPathCpp); - if (file.password_encrypted()) { - std::string passwordCpp = password != nil ? std::string([password UTF8String]) : std::string(); - try { - file = file.decrypt(passwordCpp); - } catch (odr::WrongPasswordError &) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorWrongPassword, - @"wrong password"); - } - return NO; - } - } - - if (!file.is_document_file()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnsupportedFileType, - @"not a document file"); - } - return NO; - } - - odr::DocumentFile documentFile = file.as_document_file(); - odr::DocumentType documentType = documentFile.document_type(); - _document = documentFile.document(); - - odr::HtmlService service = odr::html::translate(*_document, cachePathCpp, config); - - // the views are picked before they are rendered: bringing all of them - // offline first would write out every slide of a presentation only to - // throw the files away again - odr::HtmlViews views = CoreWrapperSelectViews(service.list_views(), documentType); - if (views.empty()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, - @"odrcore produced no displayable page"); - } - return NO; - } - - NSMutableArray *pageNames = [[NSMutableArray alloc] init]; - NSMutableArray *pageURLs = [[NSMutableArray alloc] init]; - - if (overHttp) { - // a fresh prefix for every translation, because the web view - // caches by URL: re-translating after a password or an edit has - // to end up at an address it has not seen before - static std::atomic translation{0}; - std::string prefix = "odr" + std::to_string(++translation); - - // drops the service of the document shown before this one, whose - // pages nobody is going to ask for again. what it was translated - // into stays in the cache directory, which is the system's to - // empty rather than the server's - g_server->clear(); - g_server->connect_service(service, prefix); - - for (const odr::HtmlView &view : views) { - [pageNames addObject:[NSString stringWithUTF8String:view.name().c_str()]]; - [pageURLs addObject:CoreWrapperPageURL(prefix, view.path())]; - } - } else { - _html = service.bring_offline(outputPathCpp, views); - - for (const auto &page : _html->pages()) { - [pageNames addObject:[NSString stringWithUTF8String:page.name.c_str()]]; - [pageURLs addObject:[NSURL fileURLWithPath:[NSString stringWithUTF8String:page.path.c_str()]]]; - } - } - - _pageNames = pageNames; - _pageURLs = pageURLs; - - return YES; - } catch (odr::UnknownFileType &) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnsupportedFileType, - @"unknown file type"); - } - return NO; - } catch (std::exception &e) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, - [NSString stringWithUTF8String:e.what()]); - } - return NO; - } catch (...) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, @"unknown failure in odrcore"); - } - return NO; - } - } -} - -+ (BOOL)isServedURL:(NSURL *)url { - std::uint32_t port = g_serverPort.load(); - - return port != 0 && [url.scheme isEqualToString:@"http"] - && [url.host isEqualToString:kCoreWrapperHttpHost] - && url.port.unsignedIntValue == port; -} - -- (BOOL)backTranslate:(NSString *)diff into:(NSString *)outputPath error:(NSError **)error { - @synchronized(self) { - if (!_document.has_value()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, - @"no document has been translated yet"); - } - return NO; - } - - try { - odr::html::edit(*_document, [diff UTF8String]); - _document->save([outputPath UTF8String]); - - return YES; - } catch (std::exception &e) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, - [NSString stringWithUTF8String:e.what()]); - } - return NO; - } catch (...) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, @"unknown failure in odrcore"); - } - return NO; - } - } -} - -@end diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift new file mode 100644 index 0000000..c09c357 --- /dev/null +++ b/OpenDocumentReader/CoreWrapper.swift @@ -0,0 +1,188 @@ +// +// CoreWrapper.swift +// OpenDocument Reader +// +// Replaces the ObjC++ CoreWrapper that talked to odrcore's C++ API directly. +// odrcore now arrives as the `OdrCore` Swift package, so the app links a +// prebuilt xcframework instead of building the library through conan. +// + +import Foundation +import OdrCore +import OdrCoreObjC + +let CoreWrapperErrorDomain = "app.opendocument.CoreWrapperErrorDomain" + +@objc enum CoreWrapperError: Int { + /// odrcore threw something we have no specific handling for. + case unknown = 1 + /// The document is encrypted and the supplied password did not open it. + case wrongPassword = 2 + /// Not a document odrcore can translate. PDFs land here on purpose. + case unsupportedFileType = 3 +} + +private func coreWrapperError(_ code: CoreWrapperError, _ description: String) -> NSError { + NSError( + domain: CoreWrapperErrorDomain, code: code.rawValue, + userInfo: [NSLocalizedDescriptionKey: description]) +} + +/// The one server the app has, brought up on first use and left running. +private final class PageServer { + static let shared = PageServer() + + private let lock = NSLock() + private var server: HttpServer? + private var handle: HttpServer.ServerHandle? + private var translation: UInt64 = 0 + + /// Bound port, or 0 when there is no server. + var port: UInt32 { lock.withLock { handle?.port ?? 0 } } + + /// Connects `service` under a prefix nothing has been served under before, + /// and returns the base URL for it. Nil if the socket could not be opened. + /// + /// A fresh prefix every time because the web view caches by URL: + /// re-translating after a password or an edit has to end up at an address + /// it has not seen. + func connect(_ service: HtmlService) -> (prefix: String, base: URL)? { + lock.lock() + defer { lock.unlock() } + + if server == nil { + let server = HttpServer() + guard let handle = try? server.serve() else { return nil } + self.server = server + self.handle = handle + } + guard let server, let handle else { return nil } + + translation += 1 + let prefix = "odr\(translation)" + + // drops the service of the document shown before this one, whose pages + // nobody is going to ask for again + try? server.clear() + guard (try? server.connect(service, prefix: prefix)) != nil else { return nil } + + return (prefix, handle.url(prefix: prefix)) + } +} + +/// The view odrcore names "document" holds the whole file in one page: every +/// slide of a presentation, every page of a PDF, the entire text document. +private func isCombinedView(_ view: HtmlView) -> Bool { view.name == "document" } + +/// Picks the views to show as pages, the same way OpenDocument.droid does. +/// +/// Spreadsheets get one tab per sheet, because scrolling through every sheet of +/// a workbook in one page is not how anyone reads a spreadsheet. Everything else +/// gets the combined view and nothing else — a presentation would otherwise show +/// its slides twice, and a PDF would list one tab per page next to the tab that +/// already has them all. +private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [HtmlView] { + let isSpreadsheet = documentType == .spreadsheet + let hasCombinedView = views.contains(where: isCombinedView) + + return views.filter { view in + isSpreadsheet ? !isCombinedView(view) : (!hasCombinedView || isCombinedView(view)) + } +} + +@objc final class CoreWrapper: NSObject { + @objc private(set) var pageNames: [String] = [] + @objc private(set) var pageURLs: [URL] = [] + + private var document: OdrCoreObjC.Document? + private let lock = NSRecursiveLock() + + @objc func translate( + _ inputPath: String, + cache cachePath: String, + into outputPath: String, + with password: String?, + editable: Bool + ) throws { + lock.lock() + defer { lock.unlock() } + + pageNames = [] + pageURLs = [] + + let fileTypes = (try? DecodedFile.listFileTypes(path: inputPath)) ?? [] + guard !fileTypes.isEmpty else { + throw coreWrapperError(.unsupportedFileType, "odrcore does not recognise this file type") + } + // PDFs are handed to WKWebView instead, which renders them natively + guard !fileTypes.contains(NSNumber(value: FileType.portableDocumentFormat.rawValue)) else { + throw coreWrapperError( + .unsupportedFileType, "PDF is rendered by the web view, not by odrcore") + } + + var file = try DecodedFile.decode(path: inputPath) + if file.isPasswordEncrypted { + do { + file = try file.decrypt(withPassword: password ?? "") + } catch let error as NSError + where error.code == ODRError.wrongPassword.rawValue + { + throw coreWrapperError(.wrongPassword, "wrong password") + } + } + + guard file.isDocumentFile else { + throw coreWrapperError(.unsupportedFileType, "not a document file") + } + + let documentFile = try file.asDocumentFile() + let documentType = documentFile.documentType + let document = try documentFile.document() + self.document = document + + let config = HtmlConfig() + config.editable = editable + // resource paths are resolved relative to an output directory, and in + // server mode there is none — odrcore rejects the combination + config.relativeResourcePaths = false + + let service = try HtmlTranslator.translate( + document: document, cachePath: cachePath, config: config) + + // the views are picked before they are rendered: bringing all of them + // offline first would write out every slide of a presentation only to + // throw the files away again + let views = selectViews(service.views, documentType) + guard !views.isEmpty else { + throw coreWrapperError(.unknown, "odrcore produced no displayable page") + } + + guard let connected = PageServer.shared.connect(service) else { + throw coreWrapperError(.unknown, "could not serve the translated document") + } + + pageNames = views.map(\.name) + pageURLs = views.map { connected.base.appendingPathComponent($0.path) } + } + + @objc func backTranslate(_ diff: String, into outputPath: String) throws { + lock.lock() + defer { lock.unlock() } + + guard let document else { + throw coreWrapperError(.unknown, "no document has been translated yet") + } + + try HtmlTranslator.edit(document: document, diff: diff) + try document.save(to: outputPath) + } + + /// Whether this URL is one odrcore is serving, rather than somewhere a link + /// in the document leads. + @objc static func isServedURL(_ url: URL) -> Bool { + let port = PageServer.shared.port + + return port != 0 && url.scheme == "http" && url.host == "127.0.0.1" + && url.port.map(UInt32.init) == port + } +} diff --git a/README.md b/README.md index 9bb8984..d937a15 100644 --- a/README.md +++ b/README.md @@ -5,26 +5,23 @@ This is an iOS frontend for our C++ [OpenDocument.core](https://github.com/opend ## Setup -Our C++ dependencies come from [conan-odr-index](https://github.com/opendocument-app/conan-odr-index), -which is checked out as a submodule and exported into the local conan cache. No -private conan remote is involved. - -The helper scripts in that submodule need python 3.12 or newer — they use PEP -701 f-strings, which are a syntax error on 3.11. - -1. `git submodule update --init --depth 1 conan-odr-index` -2. `python3.12 -m venv .venv && source .venv/bin/activate` -3. install conan into that venv: `pip install -r conan-odr-index/requirements.txt` -4. `conan profile detect` -5. `conan/setup-all.sh` — exports the recipes and generates the xcconfigs for - every configuration and architecture. The first run builds odrcore and its - dependencies from source and takes a while. -6. open `OpenDocumentReader.xcodeproj` in Xcode - -Everything else comes from Swift Package Manager and is resolved by Xcode. +Open `OpenDocumentReader.xcodeproj` in Xcode. Everything comes from Swift +Package Manager and is resolved by Xcode — odrcore included, as the prebuilt +`OdrCoreObjC.xcframework` the +[OdrCore](https://github.com/opendocument-app/OpenDocument.core) package +downloads from its release. There is no conan step and no C++ toolchain to set +up. + +To try an unreleased odrcore, point the package reference at a local checkout +and build the xcframework there: + +```sh +cd ../OpenDocument.core +apple/build_xcframework.py slice && apple/build_xcframework.py assemble +``` -`conan/setup-all.sh` has to be re-run whenever the odrcore version in -`conan/conanfile.py` or the `conan-odr-index` submodule changes. +Its `Package.swift` then takes `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` from the +environment of every `xcodebuild` invocation instead of the release artifact. ## How a document reaches the screen @@ -70,9 +67,7 @@ committing; CI runs `scripts/format.sh --check` and fails on any difference. | `release` | manual upload to App Store Connect, see below | `format` needs nothing but the Xcode toolchain and reports style breakage in a -minute, so it is kept apart from the native build. Everything that does need -odrcore shares `.github/actions/setup-odrcore`, which resolves the C++ -dependencies with conan and caches them per Xcode version and profile. +minute, so it is kept apart from the build. ## Releasing diff --git a/conan-odr-index b/conan-odr-index deleted file mode 160000 index 36caf08..0000000 --- a/conan-odr-index +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 36caf08e2de1327132c7b19b57ba33b3be66264d diff --git a/conan/conandeployer.py b/conan/conandeployer.py deleted file mode 100644 index d000f5a..0000000 --- a/conan/conandeployer.py +++ /dev/null @@ -1,52 +0,0 @@ -import shutil -from pathlib import Path - - -def deploy(graph, output_folder: str, **kwargs): - project_folder = Path(__file__).parent.parent - output_folder = Path(output_folder).resolve() - - conanfile = graph.root.conanfile - conanfile.output.info(f"Custom deployer to {output_folder}") - - symlinks = conanfile.conf.get( - "tools.deployer:symlinks", check_type=bool, default=True - ) - arch = conanfile.settings.get_safe("arch") - - conanfile.output.info(f"Symlinks: {symlinks}") - conanfile.output.info(f"Arch: {arch}") - - deps = {dep.ref.name: dep for dep in conanfile.dependencies.values()} - - print(f"Dependencies: {list(deps.keys())}") - - copytree_kwargs = {"symlinks": symlinks, "dirs_exist_ok": True} - - conan_files = output_folder / "files" - - if "odrcore" in deps: - dep = deps["odrcore"] - conanfile.output.info(f"Deploying odrcore to {conan_files}") - shutil.copytree( - f"{dep.package_folder}/share", - f"{conan_files}/odrcore", - **copytree_kwargs, - ) - - # odrcore 6 dropped the pdf2htmlEX and wvWare backends, so pdf2htmlex, - # poppler-data and fontconfig are no longer in the graph and nothing but - # odrcore's own data is left to deploy - - with ( - open(f"{output_folder}/input-files.xcfilelist", "w") as f_in, - open(f"{output_folder}/output-files.xcfilelist", "w") as f_out, - ): - for file in Path(conan_files).glob("**/*"): - file = file.relative_to(conan_files) - if file.suffix == ".xcfilelist": - continue - f_in.write(f"$(PROJECT_DIR)/{conan_files.relative_to(project_folder)}/{file}\n") - f_out.write( - f"${{TARGET_BUILD_DIR}}/${{UNLOCALIZED_RESOURCES_FOLDER_PATH}}/{file}\n" - ) diff --git a/conan/conanfile.py b/conan/conanfile.py deleted file mode 100644 index 92563e4..0000000 --- a/conan/conanfile.py +++ /dev/null @@ -1,23 +0,0 @@ -from conan import ConanFile -from conan.tools.apple import XcodeDeps, XcodeToolchain - -class Pkg(ConanFile): - settings = "os", "compiler", "arch", "build_type" - options = {"configuration": ["Debug", "Debug Lite", "Release", "Release Lite"]} - default_options = { - "configuration": "Debug", - "odrcore/*:shared": False, - "odrcore/*:with_libmagic": False, - "odrcore/*:with_http_server": True, - "odrcore/*:with_cli": False, - } - requires = "odrcore/6.0.0" - - def generate(self): - xcode = XcodeDeps(self) - xcode.configuration = self.options.configuration - xcode.generate() - - tc = XcodeToolchain(self) - tc.configuration = self.options.configuration - tc.generate() diff --git a/conan/copy-resources.sh b/conan/copy-resources.sh deleted file mode 100755 index 60a8e3c..0000000 --- a/conan/copy-resources.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -set -e -set -u -set -o pipefail - -mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" - -rsync -av "${PROJECT_DIR}/conan-assets/files/" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/" diff --git a/conan/profiles/ios b/conan/profiles/ios deleted file mode 100644 index ab40f92..0000000 --- a/conan/profiles/ios +++ /dev/null @@ -1,5 +0,0 @@ -include(ios-base) - -[settings] -os.sdk=iphoneos -arch=armv8 diff --git a/conan/profiles/ios-base b/conan/profiles/ios-base deleted file mode 100644 index e8ab16c..0000000 --- a/conan/profiles/ios-base +++ /dev/null @@ -1,21 +0,0 @@ -include(default) - -[settings] -os=iOS -os.version=15.0 - -compiler.cppstd=20 - -# dependencies are plain release builds, odrcore keeps debug info so crash -# reports from the field stay symbolicatable -build_type=Release -odrcore/*:build_type=RelWithDebInfo - -[conf] -# with the http server enabled, cpp-httplib resolves host names through CFHost -# on Apple platforms. odrcore's CMake links neither CoreFoundation nor CFNetwork -# (where CFHost lives), so its `cli/server` tool fails to link - the library -# itself builds fine, and the app links both frameworks itself. Only executables -# need this, hence exelinkflags. (`ODR_CLI=OFF` would be the tidier fix but -# odrcore 5.5.0 installs the cli targets whether or not it built them.) -odrcore/*:tools.build:exelinkflags=["-framework", "CoreFoundation", "-framework", "CFNetwork"] diff --git a/conan/profiles/ios-simulator-arm64 b/conan/profiles/ios-simulator-arm64 deleted file mode 100644 index 02ec81b..0000000 --- a/conan/profiles/ios-simulator-arm64 +++ /dev/null @@ -1,5 +0,0 @@ -include(ios-base) - -[settings] -os.sdk=iphonesimulator -arch=armv8 diff --git a/conan/profiles/ios-simulator-x64 b/conan/profiles/ios-simulator-x64 deleted file mode 100644 index d3ebbc8..0000000 --- a/conan/profiles/ios-simulator-x64 +++ /dev/null @@ -1,5 +0,0 @@ -include(ios-base) - -[settings] -os.sdk=iphonesimulator -arch=x86_64 diff --git a/conan/setup-all.sh b/conan/setup-all.sh deleted file mode 100755 index 31101b0..0000000 --- a/conan/setup-all.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# https://stackoverflow.com/a/246128/198996 -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -cd "$DIR/.." - -CONFIGURATIONS=("Debug" "Debug Lite" "Release" "Release Lite") - -if [ ! -f conan-odr-index/scripts/conan_export_all_packages.py ]; then - echo "conan-odr-index submodule is missing. Run:" >&2 - echo " git submodule update --init --depth 1 conan-odr-index" >&2 - exit 1 -fi - -# our recipes live in the conan-odr-index submodule and get exported into the -# local cache, so no private remote is involved -python3 conan-odr-index/scripts/conan_export_all_packages.py - -# XcodeDeps appends to the per-package include lists rather than rewriting them, -# so leftovers from earlier runs keep shadowing freshly generated files -rm -rf conan-output conan-assets - -install() { - local profile="$1" - local configuration="$2" - shift 2 - - conan install conan/ \ - --output-folder=conan-output \ - --build=missing \ - --profile:host="conan/profiles/${profile}" \ - -o "configuration=${configuration}" \ - "$@" -} - -# assets, deployed into the app bundle as odrcore's runtime data -install ios "Release" \ - --deployer=conan/conandeployer.py \ - --deployer-folder=conan-assets - -# device -for configuration in "${CONFIGURATIONS[@]}"; do - install ios "${configuration}" -done - -# simulator -for arch in "arm64" "x64"; do - for configuration in "${CONFIGURATIONS[@]}"; do - install "ios-simulator-${arch}" "${configuration}" - done -done diff --git a/configs/conan.xcconfig b/configs/conan.xcconfig deleted file mode 100644 index 8096f64..0000000 --- a/configs/conan.xcconfig +++ /dev/null @@ -1,4 +0,0 @@ -// generated by conan/setup-all.sh -// this used to sit behind four per-configuration files whose only other job was -// to include the matching Pods xcconfig and undo its LIBRARY_SEARCH_PATHS -#include "conan-output/conan_config.xcconfig"