Skip to content
Open
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
16 changes: 13 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,24 @@ jobs:
include:
- id: jvm
label: JVM/Desktop
tasks: ':reference-app:jvmTest'
tasks: ':shared:jvmTest'
kotlinDaemonHeap: '-Xmx6g -XX:+UseG1GC'
extraArgs: ''
- id: android
label: Android
tasks: ':reference-app:lintDebug'
tasks: ':shared:assembleAndroidMain :androidApp:lintDebug'
kotlinDaemonHeap: '-Xmx6g -XX:+UseG1GC'
extraArgs: ''
- id: js
label: Kotlin/JS
tasks: ':webApp:jsBrowserTest'
kotlinDaemonHeap: '-Xmx10g -XX:+UseG1GC'
extraArgs: ''
- id: wasm
label: Kotlin/Wasm
tasks: ':shared:wasmJsBrowserTest :webApp:wasmJsBrowserTest'
kotlinDaemonHeap: '-Xmx10g -XX:+UseG1GC'
extraArgs: '--max-workers=1'
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -134,4 +144,4 @@ jobs:
- name: Compile + link ${{ matrix.label }}
run: |
./gradlew --stacktrace \
:reference-app:linkDebugFramework${{ matrix.target }}
:shared:linkDebugFramework${{ matrix.target }}
28 changes: 14 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,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 :reference-app:assembleRelease
run: ./gradlew :androidApp:assembleRelease

- name: Stage outputs
shell: bash
Expand All @@ -192,7 +192,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=(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."
Expand All @@ -218,18 +218,18 @@ jobs:
include:
- os: ubuntu-latest
label: Linux deb
task: ':reference-app:packageDeb'
glob: '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: ':reference-app:packageRpm'
glob: '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: ':reference-app:packageMsi'
glob: 'reference-app/build/compose/binaries/main/msi/*.msi'
task: ':desktopApp:packageMsi'
glob: 'desktopApp/build/compose/binaries/main/msi/*.msi'
artifact: windows-msi
# Pin to the latest stable Apple Silicon image: macos-latest currently
# resolves to macOS 26, where jpackage fails during createDistributable.
Expand Down Expand Up @@ -310,13 +310,13 @@ jobs:
- name: Create distributable app image
env:
VERSION_NAME: ${{ needs.preflight.outputs.version_name }}
run: ./gradlew :reference-app:createDistributable
run: ./gradlew :desktopApp:createDistributable

- name: Tar up the distributable
shell: bash
run: |
shopt -s nullglob
APP_BASE=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[*]}"
Expand Down Expand Up @@ -362,12 +362,12 @@ jobs:
- name: Build web distribution
env:
VERSION_NAME: ${{ needs.preflight.outputs.version_name }}
run: ./gradlew :reference-app:wasmJsBrowserDistribution
run: ./gradlew :webApp:wasmJsBrowserDistribution

- name: Zip distribution for the GitHub Release
run: |
mkdir -p out
DIST=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" .)

Expand All @@ -382,7 +382,7 @@ jobs:
if: needs.preflight.outputs.is_dry_run == 'false' && needs.preflight.outputs.is_prerelease == 'false'
uses: actions/upload-pages-artifact@v3
with:
path: 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.
Expand Down Expand Up @@ -443,4 +443,4 @@ jobs:
prerelease: ${{ needs.preflight.outputs.is_prerelease == 'true' }}
generate_release_notes: true
fail_on_unmatched_files: true
files: release/*
files: release/*
132 changes: 132 additions & 0 deletions androidApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* 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")
}

android {
namespace = "dev.ohs.player.reference.app"
compileSdk = libs.versions.android.compileSdk.get().toInt()

// 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<String, String> =
providers
.fileContents(rootProject.layout.projectDirectory.file("keystore.properties"))
.asText
.map { text: String ->
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.
*/
fun nonBlankEnv(name: String): Provider<String> =
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)

// --- OAuth / OIDC config --------------------------------------------------
val localProperties: Map<String, String> =
providers
.fileContents(rootProject.layout.projectDirectory.file("local.properties"))
.asText
.map { text: String ->
val props = Properties().apply { load(text.reader()) }
props.stringPropertyNames().associateWith(props::getProperty)
}
.getOrElse(emptyMap())

fun authProp(key: String, default: String): String =
nonBlankEnv(key).orNull ?: localProperties[key]?.takeIf { it.isNotBlank() } ?: default

val resolvedRedirectScheme = authProp("OAUTH_REDIRECT_SCHEME", "dev.ohs.player.reference.app")
val resolvedRedirectHost = authProp("OAUTH_REDIRECT_HOST", "auth")

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
manifestPlaceholders["oauthRedirectScheme"] = resolvedRedirectScheme
manifestPlaceholders["oauthRedirectHost"] = resolvedRedirectHost
}
packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.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()

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_21
targetCompatibility = JavaVersion.VERSION_21
}
}

dependencies {
implementation(project(":shared"))
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose)
implementation(libs.ktor.client.okhttp)
implementation(libs.androidx.browser)
implementation(libs.androidx.work.runtime)
debugImplementation(libs.compose.uiTooling)
}
2 changes: 2 additions & 0 deletions androidApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
2 changes: 1 addition & 1 deletion build-logic/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ dependencyResolutionManagement {
from(files("../gradle/libs.versions.toml"))
}
}
}
}
78 changes: 78 additions & 0 deletions desktopApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.
*/
@file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)

import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinSerialization)
id("spotless-conventions")
}

kotlin {
jvm()

sourceSets {
jvmMain.dependencies {
implementation(project(":shared"))
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
implementation(libs.ktor.client.cio)
}
jvmTest.dependencies {
implementation(project(":shared"))
implementation(compose.desktop.currentOs)
implementation(libs.koin.test)
}
}
}

// Desktop installer version. WiX/MSI (and jpackage) require a strict numeric
// MAJOR.MINOR.PATCH, so any Semantic Version pre-release suffix is stripped here.
val composePackageVersion: String =
providers
.environmentVariable("VERSION_NAME")
.map { it.removePrefix("v").substringBefore('-') }
.filter { it.matches(Regex("""\d+\.\d+\.\d+""")) }
.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 = "PlayerReference"
packageVersion = composePackageVersion

val iconsDir = project.layout.projectDirectory.dir("../shared/desktop-icons")
macOS { iconFile.set(iconsDir.file("app-icon.icns")) }
windows { iconFile.set(iconsDir.file("app-icon.ico")) }
linux { iconFile.set(iconsDir.file("app-icon.png")) }
}
}
}

// Targets skipped on CI until their test setups are sorted out.
val isCi = providers.environmentVariable("CI").map(String::toBoolean).getOrElse(false)

if (isCi) {
// No additional CI skips needed for desktop app module
}
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ kotlin.mpp.enableCInteropCommonization=true

# Enabled parallel sync for Gradle 9.4+
org.gradle.tooling.parallel=true
kotlin.mpp.applyDefaultHierarchyTemplate=false
8 changes: 4 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
agp = "8.11.2"
agp = "9.0.1"
android-compileSdk = "37"
android-minSdk = "26"
android-targetSdk = "37"
Expand Down Expand Up @@ -37,7 +37,7 @@ ohsFhirDataCapture = "2.0.0-alpha04"
ohsFhirEngine = "2.0.0-alpha04"
ohsFhirModelR4 = "1.0.0-rc03"
ohsPlayerClient = "1.0.0-alpha03"
spotless = "8.6.0"
spotless = "8.10.1"

[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
Expand Down Expand Up @@ -71,7 +71,7 @@ koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koinBom" }
koin-compose = { module = "io.insert-koin:koin-compose" }
koin-composeViewmodel = { module = "io.insert-koin:koin-compose-viewmodel" }
koin-core = { module = "io.insert-koin:koin-core" }
koin-test = { module = "io.insert-koin:koin-test" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koinBom" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
kotlincrypto-hash-bom = { module = "org.kotlincrypto.hash:bom", version.ref = "kotlincryptoHash" }
Expand Down Expand Up @@ -106,4 +106,4 @@ composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "k
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Loading
Loading