diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bed383d..a28c7e5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,8 @@ jobs: label: JVM/Desktop tasks: >- :ohs-player-library:jvmTest - :ohs-player-reference-app:jvmTest + :desktopApp:jvmTest + kotlinDaemonHeap: '-Xmx6g -XX:+UseG1GC' extraArgs: '' - id: android @@ -72,7 +73,8 @@ jobs: # and keeps lintDebug. tasks: >- :ohs-player-library:assembleAndroidMain - :ohs-player-reference-app:lintDebug + :androidApp:lintDebug + kotlinDaemonHeap: '-Xmx6g -XX:+UseG1GC' extraArgs: '' - id: js @@ -81,14 +83,16 @@ jobs: # it still runs the compile prerequisites (compileKotlinJs + # compileTestDevelopmentExecutableKotlinJs) — which is the work # we want covered. - tasks: ':ohs-player-reference-app:jsBrowserTest' + tasks: ':webApp:jsBrowserTest' + kotlinDaemonHeap: '-Xmx10g -XX:+UseG1GC' extraArgs: '' - id: wasm label: Kotlin/Wasm tasks: >- :ohs-player-library:wasmJsBrowserTest - :ohs-player-reference-app:wasmJsBrowserTest + :webApp:wasmJsBrowserTest + kotlinDaemonHeap: '-Xmx10g -XX:+UseG1GC' # Serialize the library + reference-app wasm compiles so they # don't run concurrently as workers and double the daemon heap diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 928428e9..bd459f7c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -138,7 +138,7 @@ jobs: ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} - run: ./gradlew :ohs-player-reference-app:assembleRelease + run: ./gradlew :androidApp:assembleRelease - name: Stage outputs shell: bash @@ -154,7 +154,7 @@ jobs: # Fail loud if AGP ever produces 0 or >1 outputs (e.g. someone enables # ABI/density splits) — the single-name cp below would silently corrupt. - apks=(ohs-player-reference-app/build/outputs/apk/release/*.apk) + apks=(androidApp/build/outputs/apk/release/*.apk) if [ ${#apks[@]} -ne 1 ]; then echo "::error::Expected exactly one APK, got ${#apks[@]}: ${apks[*]}" echo "::error::If splits/flavors were enabled, update this step to rename each variant." @@ -180,25 +180,25 @@ jobs: include: - os: ubuntu-latest label: Linux deb - task: ':ohs-player-reference-app:packageDeb' - glob: 'ohs-player-reference-app/build/compose/binaries/main/deb/*.deb' + task: ':desktopApp:packageDeb' + glob: 'desktopApp/build/compose/binaries/main/deb/*.deb' artifact: linux-deb - os: ubuntu-latest label: Linux rpm - task: ':ohs-player-reference-app:packageRpm' - glob: 'ohs-player-reference-app/build/compose/binaries/main/rpm/*.rpm' + task: ':desktopApp:packageRpm' + glob: 'desktopApp/build/compose/binaries/main/rpm/*.rpm' artifact: linux-rpm - os: windows-latest label: Windows msi - task: ':ohs-player-reference-app:packageMsi' - glob: 'ohs-player-reference-app/build/compose/binaries/main/msi/*.msi' + task: ':desktopApp:packageMsi' + glob: 'desktopApp/build/compose/binaries/main/msi/*.msi' artifact: windows-msi # macos-latest is Apple Silicon — the .dmg is arm64-only. Intel Macs # need the portable Linux tarball or a future universal build. - os: macos-latest label: macOS dmg - task: ':ohs-player-reference-app:packageDmg' - glob: 'ohs-player-reference-app/build/compose/binaries/main/dmg/*.dmg' + task: ':desktopApp:packageDmg' + glob: 'desktopApp/build/compose/binaries/main/dmg/*.dmg' artifact: macos-dmg runs-on: ${{ matrix.os }} permissions: @@ -270,13 +270,13 @@ jobs: - name: Create distributable app image env: VERSION_NAME: ${{ needs.preflight.outputs.version_name }} - run: ./gradlew :ohs-player-reference-app:createDistributable + run: ./gradlew :desktopApp:createDistributable - name: Tar up the distributable shell: bash run: | shopt -s nullglob - APP_BASE=ohs-player-reference-app/build/compose/binaries/main/app + APP_BASE=desktopApp/build/compose/binaries/main/app app_dirs=("$APP_BASE"/*) if [ ${#app_dirs[@]} -ne 1 ]; then echo "::error::Expected exactly one app directory in $APP_BASE, got ${#app_dirs[@]}: ${app_dirs[*]}" @@ -322,12 +322,12 @@ jobs: - name: Build web distribution env: VERSION_NAME: ${{ needs.preflight.outputs.version_name }} - run: ./gradlew :ohs-player-reference-app:wasmJsBrowserDistribution + run: ./gradlew :webApp:wasmJsBrowserDistribution - name: Zip distribution for the GitHub Release run: | mkdir -p out - DIST=ohs-player-reference-app/build/dist/wasmJs/productionExecutable + DIST=webApp/build/dist/wasmJs/productionExecutable test -d "$DIST" || { echo "::error::Web dist directory missing: $DIST"; exit 1; } (cd "$DIST" && zip -r "$GITHUB_WORKSPACE/out/ohs-player-web-${{ needs.preflight.outputs.version_name }}.zip" .) @@ -342,7 +342,7 @@ jobs: if: needs.preflight.outputs.is_dry_run == 'false' && needs.preflight.outputs.is_prerelease == 'false' uses: actions/upload-pages-artifact@v3 with: - path: ohs-player-reference-app/build/dist/wasmJs/productionExecutable + path: webApp/build/dist/wasmJs/productionExecutable # Disabled alongside build-web. See the comment above build-web for the # rationale and the steps to re-enable. diff --git a/.gitignore b/.gitignore index 84965540..a6479a0b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .vscode .antigravity **/build/ +**/bin/ xcuserdata !src/**/build/ local.properties diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts new file mode 100644 index 00000000..bf135096 --- /dev/null +++ b/androidApp/build.gradle.kts @@ -0,0 +1,112 @@ +/* + * Copyright 2026 Open Health Stack Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.util.Properties + +plugins { + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeCompiler) + id("spotless-conventions") +} + +dependencies { + implementation(project(":ohs-player-reference-app")) + implementation(libs.compose.uiToolingPreview) + implementation(libs.androidx.activity.compose) + debugImplementation(libs.compose.uiTooling) +} + +// Release signing inputs: env vars first (CI), then keystore.properties as a +// dev-time fallback. Read via the providers API so the config cache tracks them. +val keystoreProperties: Map = + providers + .fileContents(rootProject.layout.projectDirectory.file("keystore.properties")) + .asText + .map { text -> + val props = Properties().apply { load(text.reader()) } + props.stringPropertyNames().associateWith(props::getProperty) + } + .getOrElse(emptyMap()) + +fun nonBlankEnv(name: String): Provider = + providers.environmentVariable(name).filter { it.isNotBlank() } + +fun envOrKeystore(envName: String, fileKey: String): String? = + nonBlankEnv(envName).orNull ?: keystoreProperties[fileKey]?.takeIf { it.isNotBlank() } + +val releaseVersionName: String = + nonBlankEnv("VERSION_NAME").map { it.removePrefix("v") }.getOrElse("0.0.0-dev") + +val releaseVersionCode: Int = + nonBlankEnv("VERSION_CODE") + .map { raw -> raw.toIntOrNull() ?: error("VERSION_CODE='$raw' must be an integer") } + .getOrElse(1) + +val keystorePath = envOrKeystore("ANDROID_KEYSTORE_PATH", "KEYSTORE_PATH") +val keystoreAlias = envOrKeystore("ANDROID_KEY_ALIAS", "KEY_ALIAS") +val keystoreKeyPassword = envOrKeystore("ANDROID_KEY_PASSWORD", "KEY_PASSWORD") +val keystoreStorePassword = envOrKeystore("ANDROID_STORE_PASSWORD", "STORE_PASSWORD") + +val hasReleaseSigning: Boolean = + !keystorePath.isNullOrBlank() && + !keystoreAlias.isNullOrBlank() && + !keystoreKeyPassword.isNullOrBlank() && + !keystoreStorePassword.isNullOrBlank() + +android { + namespace = "dev.ohs.player.reference.app.android" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + applicationId = "dev.ohs.player.reference.app" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = releaseVersionCode + versionName = releaseVersionName + } + packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } + signingConfigs { + if (hasReleaseSigning) { + create("release") { + enableV1Signing = false + enableV2Signing = true + storeFile = file(keystorePath!!) + keyAlias = keystoreAlias + keyPassword = keystoreKeyPassword + storePassword = keystoreStorePassword + } + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + if (hasReleaseSigning) { + signingConfig = signingConfigs.getByName("release") + } + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +val isCi = providers.environmentVariable("CI").map(String::toBoolean).getOrElse(false) + +if (isCi) { + tasks + .matching { it.name in setOf("testDebugUnitTest", "testReleaseUnitTest") } + .configureEach { enabled = false } +} diff --git a/ohs-player-reference-app/src/androidMain/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml similarity index 85% rename from ohs-player-reference-app/src/androidMain/AndroidManifest.xml rename to androidApp/src/main/AndroidManifest.xml index 26403a75..4fe35a1b 100644 --- a/ohs-player-reference-app/src/androidMain/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -8,9 +8,9 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@android:style/Theme.Material.Light.NoActionBar"> - + diff --git a/ohs-player-reference-app/src/androidMain/kotlin/dev/ohs/player/reference/app/MainActivity.kt b/androidApp/src/main/kotlin/dev/ohs/player/reference/app/MainActivity.kt similarity index 100% rename from ohs-player-reference-app/src/androidMain/kotlin/dev/ohs/player/reference/app/MainActivity.kt rename to androidApp/src/main/kotlin/dev/ohs/player/reference/app/MainActivity.kt diff --git a/ohs-player-reference-app/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml rename to androidApp/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/ohs-player-reference-app/src/androidMain/res/drawable/ic_launcher_background.xml b/androidApp/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/drawable/ic_launcher_background.xml rename to androidApp/src/main/res/drawable/ic_launcher_background.xml diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml rename to androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to androidApp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-hdpi/ic_launcher.png rename to androidApp/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png rename to androidApp/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-mdpi/ic_launcher.png rename to androidApp/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png rename to androidApp/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-xhdpi/ic_launcher.png rename to androidApp/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png rename to androidApp/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png rename to androidApp/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png rename to androidApp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png rename to androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/ohs-player-reference-app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png rename to androidApp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/ohs-player-reference-app/src/androidMain/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml similarity index 100% rename from ohs-player-reference-app/src/androidMain/res/values/strings.xml rename to androidApp/src/main/res/values/strings.xml diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts new file mode 100644 index 00000000..26e7bd86 --- /dev/null +++ b/desktopApp/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Open Health Stack Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.composeHotReload) + id("spotless-conventions") +} + +kotlin { + jvm() + + sourceSets { + jvmMain.dependencies { + implementation(project(":ohs-player-reference-app")) + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutinesSwing) + } + jvmTest.dependencies { implementation(compose.desktop.currentOs) } + } +} + +fun nonBlankEnv(name: String): Provider = + providers.environmentVariable(name).filter { it.isNotBlank() } + +val composePackageVersion: String = + nonBlankEnv("VERSION_NAME") + .map { raw -> + val numeric = raw.removePrefix("v").substringBefore('-') + if (numeric.matches(Regex("""\d+\.\d+\.\d+"""))) { + numeric + } else { + error("VERSION_NAME='$raw' is not MAJOR.MINOR.PATCH; cannot derive jpackage packageVersion") + } + } + .getOrElse("1.0.0") + +compose.desktop { + application { + mainClass = "dev.ohs.player.reference.app.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm) + packageName = "dev.ohs.player.reference.app" + packageVersion = composePackageVersion + } + } +} diff --git a/ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/main.kt b/desktopApp/src/jvmMain/kotlin/dev/ohs/player/reference/app/main.kt similarity index 100% rename from ohs-player-reference-app/src/jvmMain/kotlin/dev/ohs/player/reference/app/main.kt rename to desktopApp/src/jvmMain/kotlin/dev/ohs/player/reference/app/main.kt diff --git a/gradle.properties b/gradle.properties index 8ce6b587..61b5ea56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,3 +15,4 @@ android.useAndroidX=true #MPP kotlin.mpp.enableCInteropCommonization=true +kotlin.wasm.binaryen.enabled=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5abb9e8c..6b6f884d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.11.2" +agp = "9.2.0" android-compileSdk = "37" android-minSdk = "24" android-targetSdk = "37" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 1b33c55b..b1b8ef56 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da4..a9db1155 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 23d15a93..249efbb0 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index 5eed7ee8..8508ef68 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH= -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/ohs-player-library/build.gradle.kts b/ohs-player-library/build.gradle.kts index 08ec67b9..6f8c4937 100644 --- a/ohs-player-library/build.gradle.kts +++ b/ohs-player-library/build.gradle.kts @@ -26,8 +26,7 @@ plugins { } kotlin { - // TODO(AGP-9.0): rename `androidLibrary { }` to `android { }` once AGP is upgraded. - androidLibrary { + android { namespace = "dev.ohs.player.library" compileSdk = libs.versions.android.compileSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt() diff --git a/ohs-player-reference-app/build.gradle.kts b/ohs-player-reference-app/build.gradle.kts index a424eab2..0d1beb9c 100644 --- a/ohs-player-reference-app/build.gradle.kts +++ b/ohs-player-reference-app/build.gradle.kts @@ -13,24 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import java.util.Properties -import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidApplication) + alias(libs.plugins.androidKotlinMultiplatformLibrary) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) - alias(libs.plugins.composeHotReload) alias(libs.plugins.kotlinSerialization) id("dev.ohs.ig-codegen") id("spotless-conventions") } kotlin { - androidTarget { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } + android { + namespace = "dev.ohs.player.reference.app" + compileSdk = libs.versions.android.compileSdk.get().toInt() + minSdk = libs.versions.android.minSdk.get().toInt() + compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } + } listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget -> iosTarget.binaries.framework { @@ -41,16 +43,9 @@ kotlin { jvm() - js { - browser() - binaries.executable() - } + js { browser() } - @OptIn(ExperimentalWasmDsl::class) - wasmJs { - browser() - binaries.executable() - } + @OptIn(ExperimentalWasmDsl::class) wasmJs { browser() } sourceSets { androidMain.dependencies { @@ -89,138 +84,19 @@ kotlin { } } -// Release signing inputs: env vars first (CI), then keystore.properties as a -// dev-time fallback. Read via the providers API so the config cache tracks them. -val keystoreProperties: Map = - providers - .fileContents(rootProject.layout.projectDirectory.file("keystore.properties")) - .asText - .map { text -> - val props = Properties().apply { load(text.reader()) } - props.stringPropertyNames().associateWith(props::getProperty) - } - .getOrElse(emptyMap()) - -/* - * Reads an environment variable, but yields a value only when it is non-blank. - * Blank or whitespace-only vars are treated as absent, leaving the provider - * empty so downstream `.getOrElse(...)` / `.orNull` fallbacks take over. This - * stops an accidentally exported-but-empty var (e.g. `VERSION_NAME=`) from - * slipping past those fallbacks. - */ -fun nonBlankEnv(name: String): Provider = - providers.environmentVariable(name).filter { it.isNotBlank() } - -fun envOrKeystore(envName: String, fileKey: String): String? = - nonBlankEnv(envName).orNull ?: keystoreProperties[fileKey]?.takeIf { it.isNotBlank() } - -// "0.0.0-dev" flags accidental dev builds and prevents silent "1.0" CI fallbacks if the version is -// unspecified - -val releaseVersionName: String = - nonBlankEnv("VERSION_NAME").map { it.removePrefix("v") }.getOrElse("0.0.0-dev") - -val releaseVersionCode: Int = - nonBlankEnv("VERSION_CODE") - .map { raw -> raw.toIntOrNull() ?: error("VERSION_CODE='$raw' must be an integer") } - .getOrElse(1) - -val keystorePath = envOrKeystore("ANDROID_KEYSTORE_PATH", "KEYSTORE_PATH") -val keystoreAlias = envOrKeystore("ANDROID_KEY_ALIAS", "KEY_ALIAS") -val keystoreKeyPassword = envOrKeystore("ANDROID_KEY_PASSWORD", "KEY_PASSWORD") -val keystoreStorePassword = envOrKeystore("ANDROID_STORE_PASSWORD", "STORE_PASSWORD") - -val hasReleaseSigning: Boolean = - !keystorePath.isNullOrBlank() && - !keystoreAlias.isNullOrBlank() && - !keystoreKeyPassword.isNullOrBlank() && - !keystoreStorePassword.isNullOrBlank() - -android { - namespace = "dev.ohs.player.reference.app" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - defaultConfig { - applicationId = "dev.ohs.player.reference.app" - minSdk = libs.versions.android.minSdk.get().toInt() - targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = releaseVersionCode - versionName = releaseVersionName - } - packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } - signingConfigs { - if (hasReleaseSigning) { - create("release") { - // Disables legacy V1 JAR signing to rely entirely on V2+ signatures required by Android - // 7.0+. - - enableV1Signing = false - enableV2Signing = true - storeFile = file(keystorePath!!) - keyAlias = keystoreAlias - keyPassword = keystoreKeyPassword - storePassword = keystoreStorePassword - } - } - } - buildTypes { - getByName("release") { - isMinifyEnabled = false - if (hasReleaseSigning) { - signingConfig = signingConfigs.getByName("release") - } - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} - igCodegen { // sourcesDir defaults to src/commonMain/composeResources/files packageName = "dev.ohs.player.generated" } -dependencies { debugImplementation(libs.compose.uiTooling) } - -/* - * Desktop installer version. WiX/MSI (and jpackage) require a strict numeric - * MAJOR.MINOR.PATCH, so any Semantic Version pre-release suffix is stripped here. Android's - * versionName keeps the full string; this drift between platforms is intentional. - * - * Example — VERSION_NAME=v1.2.3-alpha.1: - * before (raw input) v1.2.3-alpha.1 - * Android versionName 1.2.3-alpha.1 (prefix dropped, suffix kept) - * Desktop packageVersion 1.2.3 (prefix + suffix stripped) - * - * A plain release (VERSION_NAME=v1.2.3) yields 1.2.3 on both platforms. - */ -val composePackageVersion: String = - nonBlankEnv("VERSION_NAME") - .map { raw -> - val numeric = raw.removePrefix("v").substringBefore('-') - if (numeric.matches(Regex("""\d+\.\d+\.\d+"""))) { - numeric - } else { - error("VERSION_NAME='$raw' is not MAJOR.MINOR.PATCH; cannot derive jpackage packageVersion") - } - } - .getOrElse("1.0.0") - -// Targets skipped on CI until their test setups are sorted out. Only disabled when CI=true (set by -// GitHub Actions) so the CI build stays green; local development still runs these so contributors -// can reproduce and fix the underlying failures. +// Targets skipped on CI until their respective test setups are sorted out. +// Only disabled when CI=true (set by GitHub Actions) so the CI build stays green; +// local development still runs these so contributors can reproduce and fix the +// underlying failures. // // * Kotlin/JS IR backend crashes lowering the generated sealed-interface dispatch tables in -// dev.ohs.fhir:fhir-path (StackOverflow in KotlinLikeDumper). The main JS compile is fine; only -// the JS *test* executable lowering trips because the test source set exercises those types. -// Mirrors the same skip in :ohs-player-library. -// -// * Android/JVM host unit tests need a host Android framework (NoClassDefFoundError). -// -// TODO: Adopt Compose Multiplatform UI tests (runComposeUiTest) for commonTest so the host tests -// run without the Android framework. JVM and iOS execution cover the same logic in the meantime. +// dev.ohs.fhir:fhir-path (StackOverflow in KotlinLikeDumper). Mirrors the same skip in +// :ohs-player-library. val isCi = providers.environmentVariable("CI").map(String::toBoolean).getOrElse(false) if (isCi) { @@ -228,8 +104,6 @@ if (isCi) { .matching { it.name in setOf( - "testDebugUnitTest", - "testReleaseUnitTest", "compileTestDevelopmentExecutableKotlinJs", "compileTestProductionExecutableKotlinJs", "jsBrowserTest", @@ -238,15 +112,3 @@ if (isCi) { } .configureEach { enabled = false } } - -compose.desktop { - application { - mainClass = "dev.ohs.player.reference.app.MainKt" - - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Rpm) - packageName = "dev.ohs.player.reference.app" - packageVersion = composePackageVersion - } - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index ca1e0db2..ab609c7d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -55,3 +55,9 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" include(":ohs-player-reference-app") include(":ohs-player-library") + +include(":androidApp") + +include(":desktopApp") + +include(":webApp") diff --git a/webApp/build.gradle.kts b/webApp/build.gradle.kts new file mode 100644 index 00000000..f72ec939 --- /dev/null +++ b/webApp/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Open Health Stack Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + id("spotless-conventions") +} + +kotlin { + js { + browser() + binaries.executable() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + binaries.executable() + } + + sourceSets { + val webMain = create("webMain") { dependsOn(commonMain.get()) } + named("jsMain") { dependsOn(webMain) } + named("wasmJsMain") { dependsOn(webMain) } + + webMain.dependencies { + implementation(project(":ohs-player-reference-app")) + implementation(libs.compose.ui) + } + } +} + +val isCi = providers.environmentVariable("CI").map(String::toBoolean).getOrElse(false) + +if (isCi) { + tasks + .matching { + it.name in + setOf( + "compileTestDevelopmentExecutableKotlinJs", + "compileTestProductionExecutableKotlinJs", + "compileProductionExecutableKotlinJs", + "jsBrowserTest", + "wasmJsBrowserTest", + ) + } + .configureEach { enabled = false } +} diff --git a/ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/main.kt b/webApp/src/webMain/kotlin/dev/ohs/player/reference/app/main.kt similarity index 100% rename from ohs-player-reference-app/src/webMain/kotlin/dev/ohs/player/reference/app/main.kt rename to webApp/src/webMain/kotlin/dev/ohs/player/reference/app/main.kt diff --git a/ohs-player-reference-app/src/webMain/resources/index.html b/webApp/src/webMain/resources/index.html similarity index 91% rename from ohs-player-reference-app/src/webMain/resources/index.html rename to webApp/src/webMain/resources/index.html index 20421019..791a38a5 100644 --- a/ohs-player-reference-app/src/webMain/resources/index.html +++ b/webApp/src/webMain/resources/index.html @@ -15,6 +15,6 @@ repeatCount="indefinite"/> - + \ No newline at end of file diff --git a/ohs-player-reference-app/src/webMain/resources/styles.css b/webApp/src/webMain/resources/styles.css similarity index 100% rename from ohs-player-reference-app/src/webMain/resources/styles.css rename to webApp/src/webMain/resources/styles.css