Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ jobs:
- name: setup gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6

# every ABI's artifact carries the same assets/, so they merge into the
# one prebuilt tree the gradle build reads
# each ABI's artifact carries its own jniLibs/<abi>, so they merge into
# the one prebuilt tree the gradle build reads
- name: download native libraries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/apple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ jobs:

# The iOS *device* slice is only ever link checked; nothing runs it. The
# simulator run is the analogue of android's instrumented job and the only one
# that sees what a device sees — that `+load` fired, that `NSBundle` found the
# renderer assets, that a temp directory is writable inside an app container.
# that sees what a device sees — that the framework loads, that rendering
# works unconfigured, that a temp directory is writable in an app container.
test:
needs: xcframework
runs-on: macos-15
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ jobs:
-DCMAKE_CXX_FLAGS="-Werror"
-DCMAKE_INSTALL_PREFIX=install
-DODR_TEST=ON
-DODR_BUNDLE_ASSETS=ON
${{ matrix.bindings && '-DODR_JNI=ON -DODR_PYTHON=ON' || '' }}

- name: cmake
Expand Down Expand Up @@ -170,7 +169,6 @@ jobs:
install
build/test/odr_test
build/test/Release/odr_test.exe
build/data
if-no-files-found: error
include-hidden-files: true

Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,11 @@ jobs:
restore-keys: |
ccache-${{ env.CACHE_FLAVOR }}-${{ matrix.host_profile }}-${{ env.CCACHE_KEY_SUFFIX }}-

# Match the dependency set that pyproject.toml enables. `bundle_assets`
# is what stages the renderer's css/js so they land in `pyodr/data`.
# Match the dependency set that pyproject.toml enables.
- name: conan install
run: >
conan install .
-o '&:with_python=True'
-o '&:bundle_assets=True'
--profile:host '${{ matrix.host_profile }}'
--profile:build '${{ matrix.build_profile }}'
--build missing
Expand Down
24 changes: 8 additions & 16 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ option(ODR_CLANG_TIDY "Run clang-tidy static analysis" OFF)
# containers, which libmagic cannot, and it costs neither a dependency nor the
# 8 MB `magic.mgc` database every consumer had to ship and point us at.
option(ODR_WITH_LIBMAGIC "Removed, does nothing (deprecated)" OFF)
option(ODR_BUNDLE_ASSETS "Bundle assets during build and install" OFF)
option(ODR_BUNDLE_ASSETS "Removed, does nothing (deprecated)" OFF)
option(ODR_PYTHON "Build Python bindings" OFF)
option(ODR_JNI "Build JNI bindings" OFF)
option(ODR_APPLE "Build Objective-C bindings as a framework" OFF)
Expand Down Expand Up @@ -61,13 +61,6 @@ find_package(uchardet REQUIRED)
find_package(utf8cpp REQUIRED)
find_package(argon2 REQUIRED)

include(FetchContent)
FetchContent_Declare(odr.js
URL "https://github.com/opendocument-app/OpenDocument.js/releases/download/v2.0.0/build.zip"
URL_HASH SHA256=5ccbe1c5bb46d65bc0dccc6c6df6548a571eef5536fcb95f9dcb80add26800b6
)
FetchContent_MakeAvailable(odr.js)

set(PRE_CONFIGURE_FILE "src/odr/internal/git_info.cpp.in")
set(POST_CONFIGURE_FILE "${CMAKE_CURRENT_BINARY_DIR}/src/odr/internal/git_info.cpp")
# An explicitly injected `GIT_HEAD_SHA1` wins over the working tree, which is
Expand Down Expand Up @@ -141,6 +134,7 @@ set(ODR_SOURCE_FILES
"src/odr/internal/html/document_element.cpp"
"src/odr/internal/html/filesystem.cpp"
"src/odr/internal/html/font_file.cpp"
"src/odr/internal/html/frontend.cpp"
"src/odr/internal/html/html_service.cpp"
"src/odr/internal/html/html_writer.cpp"
"src/odr/internal/html/image_file.cpp"
Expand Down Expand Up @@ -274,10 +268,6 @@ target_link_libraries(odr
argon2::argon2
)

set(ODR_BUILD_ODR_DATA_PATH "${CMAKE_CURRENT_BINARY_DIR}/data")
file(COPY "${odr.js_SOURCE_DIR}/" DESTINATION "${ODR_BUILD_ODR_DATA_PATH}")
set(ODR_INSTALL_ODR_DATA_PATH "${CMAKE_INSTALL_DATADIR}")

if (ODR_WITH_HTTP_SERVER)
find_package(httplib REQUIRED)
target_sources(odr
Expand All @@ -299,6 +289,12 @@ if (ODR_WITH_LIBMAGIC)
"`odr::mimetype` is our own detection. Drop the option; "
"`GlobalParams::libmagic_database_path` is inert too.")
endif ()
if (ODR_BUNDLE_ASSETS)
message(DEPRECATION
"ODR_BUNDLE_ASSETS no longer does anything: the css and js are "
"part of the library and there is nothing left to ship. Drop the "
"option; `GlobalParams::odr_core_data_path` is inert too.")
endif ()

configure_file("src/odr/internal/project_info.cpp.in" "src/odr/internal/project_info.cpp")

Expand Down Expand Up @@ -355,7 +351,3 @@ install(
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
)
install(
DIRECTORY "${ODR_BUILD_ODR_DATA_PATH}/"
DESTINATION "${ODR_INSTALL_ODR_DATA_PATH}"
)
6 changes: 3 additions & 3 deletions android/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ first — the java API and its android constraints live there. User facing docs:

| Path | What |
|------|------|
| `build.gradle.kts` | The library module: sources from `../jni/java`, prebuilt native libs and assets, lint, publishing. Single project — `rootProject.name` *is* the artifactId. |
| `build_native.py` | conan + cmake per ABI → `native/prebuilt/{jniLibs,assets}`. Invoked by the `buildNative` gradle task and directly by CI. |
| `src/main/java/.../android/OdrAndroid.kt` | The only android specific production code: extracts the bundled assets and registers them with `GlobalParams`. |
| `build.gradle.kts` | The library module: sources from `../jni/java`, prebuilt native libs, lint, publishing. Single project — `rootProject.name` *is* the artifactId. |
| `build_native.py` | conan + cmake per ABI → `native/prebuilt/jniLibs`. Invoked by the `buildNative` gradle task and directly by CI. |
| `src/main/java/.../android/OdrAndroid.kt` | The only android specific production code, and a deprecated no-op: it used to extract the renderer's assets, which are part of the library now. |
| `src/androidTest/` | Instrumented suite, JUnit 4 + androidx.test, inputs from `../jni/testfixtures`. |
| `consumer-rules.pro` | Keeps `app.opendocument.core.**` — JNI resolves it by name, R8 cannot see that. |

Expand Down
21 changes: 6 additions & 15 deletions android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ odr-core-android.aar
├── classes.jar app.opendocument.core (../jni/java) + OdrAndroid
├── jni/<abi>/libodr_jni.so the bindings with the core linked in
├── jni/<abi>/libc++_shared.so the c++ runtime they were built against
├── assets/core/odrcore/* css/js of the html renderer
└── proguard.txt keeps the classes JNI resolves by name
```

Expand All @@ -33,22 +32,16 @@ Packages needs a token with `read:packages` even for public packages — see

```kotlin
import app.opendocument.core.*
import app.opendocument.core.android.OdrAndroid

OdrAndroid.init(context) // extracts the bundled assets, loads the library

val file = Odr.open(path)
val service = Html.translate(file, cacheDir.path, HtmlConfig())
val html = service.bringOffline(outputDir.path)
```

The java API is java and stays callable as such (`OdrAndroid.init(context)` is
a static method, and it still throws a checked `IOException`); only this
module's own code — `OdrAndroid` and the instrumented suite — is kotlin.

`OdrAndroid.init` is idempotent and cheap after the first call: the assets are
unpacked once per library build, into the app's no-backup storage, and the
library is pointed at them via `GlobalParams`.
Nothing needs initialising: the renderer's css and js are part of the native
library. `OdrAndroid.init(context)` is a deprecated no-op — it used to unpack
the assets that carried them — and stays callable, checked `IOException` and
all, so apps written against the older AAR keep compiling.

Serving the rendered HTML through `HttpServer` needs two things from the app,
neither of which a library may decide on its own: `android.permission.INTERNET`,
Expand Down Expand Up @@ -93,8 +86,7 @@ The odrcore build is a normal one — `ODR_JNI=ON`, static core linked into
```

The instrumented suite (`src/androidTest`) is the part that sees what a device
sees: it loads the native library, extracts and reads the bundled assets,
decodes and renders documents, drives a java log sink from native code, and
sees: it loads the native library, decodes and renders documents, drives a java log sink from native code, and
serves a document over HTTP. Its inputs come from `../jni/testfixtures`, the
same ones the host junit suite uses.

Expand Down Expand Up @@ -129,8 +121,7 @@ that click is the last point at which a bad artifact can be dropped rather than
lived with.

For now OpenDocument.droid keeps building odrcore from the conan package
(`with_jni=True`), deploying `libodr_jni.so`, `odr-core-java.jar` and the assets
out of it. That path is unaffected by anything here, and it is the reason the
(`with_jni=True`), deploying `libodr_jni.so` and `odr-core-java.jar` out of it. That path is unaffected by anything here, and it is the reason the
two halves cannot drift: they come out of one build.

So the AAR is, for now, a second packaging of that same build — for consumers
Expand Down
22 changes: 9 additions & 13 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,17 @@ val buildNative =
enabled = nativeAbis.isNotEmpty()
}

// An AAR without the native library or without its assets builds and publishes
// happily and then fails at runtime in whatever app picked it up, so make the
// absence a build error.
// An AAR without the native library builds and publishes happily and then
// fails at runtime in whatever app picked it up, so make the absence a build
// error.
val checkNative =
tasks.register("checkNative") {
dependsOn(buildNative)
val prebuilt = layout.projectDirectory.dir("native/prebuilt")
val required = listOf("jniLibs", "assets").map { prebuilt.dir(it).asFile }
val jniLibs = layout.projectDirectory.dir("native/prebuilt/jniLibs").asFile
doLast {
val missing = required.filter { it.list().isNullOrEmpty() }
if (missing.isNotEmpty()) {
if (jniLibs.list().isNullOrEmpty()) {
throw GradleException(
"nothing in ${missing.joinToString()} — run android/build_native.py, " +
"nothing in $jniLibs — run android/build_native.py, " +
"or pass -Podr.abis=<abis> to have the build run it"
)
}
Expand All @@ -104,10 +102,9 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}

// the instrumented apk carries the bindings and the renderer assets, and
// pushing that onto a cold emulator outlasts ddmlib's default timeout, which
// surfaces as a ShellCommandUnresponsiveException rather than as a failing
// test
// pushing the instrumented apk onto a cold emulator outlasts ddmlib's
// default timeout, which surfaces as a ShellCommandUnresponsiveException
// rather than as a failing test
installation { timeOutInMs = 10 * 60 * 1000 }

sourceSets {
Expand All @@ -116,7 +113,6 @@ android {
// artifacts cannot drift apart
java.srcDir("../jni/java")
jniLibs.srcDir("native/prebuilt/jniLibs")
assets.srcDir("native/prebuilt/assets")
}
named("androidTest") {
// the host junit suite's inputs, shared verbatim
Expand Down
21 changes: 6 additions & 15 deletions android/build_native.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
#!/usr/bin/env python3
"""Build the native half of the AAR: `libodr_jni.so` per android ABI, plus the
runtime assets, laid out the way `build.gradle.kts` expects.
"""Build the native half of the AAR: `libodr_jni.so` per android ABI, laid out
the way `build.gradle.kts` expects.

android/build_native.py --abi x86_64 --abi armv8

Each ABI gets its own conan install (`android-<arch>` host profile) and cmake
build under `android/native/build/<arch>`, and the results are copied into
`--output` (default `android/native/prebuilt`), which the gradle build reads as
its jniLibs and assets source sets:
its jniLibs source set:

prebuilt/jniLibs/<abi>/libodr_jni.so the bindings, core linked in
prebuilt/jniLibs/<abi>/libc++_shared.so from the NDK, see below
prebuilt/assets/core/odrcore/* css/js of the html renderer

`libc++_shared.so` has to be shipped because the android profiles build against
the shared c++ runtime and nothing else in a consuming app pulls it in; the
asset layout mirrors what OpenDocument.droid's conan deployer already produces.
the shared c++ runtime and nothing else in a consuming app pulls it in.
"""

import argparse
Expand Down Expand Up @@ -98,8 +96,7 @@ def build(architecture: str, conan: str, build_profile: str, output: Path) -> No
"-DODR_JNI=ON",
"-DODR_CLI=OFF",
"-DODR_TEST=OFF",
"-DODR_WITH_HTTP_SERVER=ON",
"-DODR_BUNDLE_ASSETS=ON"])
"-DODR_WITH_HTTP_SERVER=ON"])
run(["cmake", "--build", cmake_dir, "--target", "odr_jni",
"--parallel", str(os.cpu_count() or 1)])

Expand All @@ -111,12 +108,6 @@ def build(architecture: str, conan: str, build_profile: str, output: Path) -> No
shutil.copy2(source, target)
strip(ndk, target)

# architecture independent, so the last ABI built simply wins
data = cmake_dir / "data"
assets = output / "assets" / "core"
shutil.rmtree(assets, ignore_errors=True)
shutil.copytree(data, assets / "odrcore")


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__,
Expand All @@ -128,7 +119,7 @@ def main() -> int:
parser.add_argument("--build-profile", default="default",
help="conan build profile, i.e. the profile of this machine")
parser.add_argument("--output", type=Path, default=ANDROID_ROOT / "native" / "prebuilt",
help="where to lay out jniLibs/ and assets/")
help="where to lay out jniLibs/")
args = parser.parse_args()

if not os.environ.get("ANDROID_HOME"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ class DocumentTest {

@Before
fun setUp() {
TestSupport.initialize()
tempDir = TestSupport.tempDir("document")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class HttpServerTest {

@Before
fun setUp() {
TestSupport.initialize()
tempDir = TestSupport.tempDir("http-server")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ class LoggerTest {

@Before
fun setUp() {
TestSupport.initialize()
tempDir = TestSupport.tempDir("logger")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
package app.opendocument.core

import androidx.test.ext.junit.runners.AndroidJUnit4
import java.io.File
import app.opendocument.core.android.OdrAndroid
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/** The AAR's own contract: the native library loads and the bundled assets end up on disk. */
/** The AAR's own contract: the native library loads and works without any setup. */
@RunWith(AndroidJUnit4::class)
class OdrAndroidTest {
@Before
fun setUp() {
TestSupport.initialize()
}

@Test
fun nativeLibraryLoads() {
// reaching the native side at all means the .so, its ABI and libc++_shared
Expand All @@ -28,18 +21,11 @@ class OdrAndroidTest {
}

@Test
fun assetsAreExtracted() {
val data = File(GlobalParams.odrCoreDataPath())
assertTrue("$data is not a directory", data.isDirectory)
assertTrue(File(data, "document.css").isFile)
assertTrue(File(data, "document.js").isFile)
}

@Test
fun initIsIdempotent() {
val dataPath = GlobalParams.odrCoreDataPath()
TestSupport.initialize()
assertEquals(dataPath, GlobalParams.odrCoreDataPath())
@Suppress("DEPRECATION")
fun initIsANoOp() {
// it is still called by apps built against the versions that needed it
OdrAndroid.init(TestSupport.context())
OdrAndroid.init(TestSupport.context())
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@ package app.opendocument.core

import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import app.opendocument.core.android.OdrAndroid
import java.io.File
import java.io.IOException
import java.nio.file.Path

/** Shared setup of the instrumented suite: the initialised library and a scratch directory. */
/** Shared setup of the instrumented suite. */
internal object TestSupport {
fun context(): Context = InstrumentationRegistry.getInstrumentation().targetContext

/** The library, with its bundled assets extracted and registered. */
fun initialize() {
OdrAndroid.init(context())
}

/** An empty directory under the app cache, named after the caller. */
fun tempDir(name: String): Path {
val directory = File(context().cacheDir, name)
Expand Down
Loading
Loading