Aller au contenu

Templating des ressources Gradle selon la cible

Cette documentation explique comment remplacer des variables dans des fichiers HTML, CSS, JavaScript ou texte avec une syntaxe de type :

@PUBLIC_BASE_URL@
@PUBLIC_API_URL@

Les remplacements sont effectués pendant le traitement des ressources. Les fichiers sources ne sont jamais modifiés.

La priorité recommandée est :

  1. Variable d’environnement CI ou locale
  2. Propriété Gradle locale
val publicBaseUrl = providers.environmentVariable("PUBLIC_BASE_URL")
.orElse(providers.gradleProperty("publicBaseUrl"))
val publicApiUrl = providers.environmentVariable("PUBLIC_API_URL")
.orElse(providers.gradleProperty("publicApiUrl"))

Exemple de gradle.properties local :

publicBaseUrl=http://localhost:8080
publicApiUrl=http://localhost:8080

Exemple CI :

PUBLIC_BASE_URL=https://example.com
PUBLIC_API_URL=https://api.example.com

Une valeur publique peut être injectée dans une ressource. Un secret ne doit jamais l’être, car il sera visible dans le fichier final.

Exemple HTML :

<link rel="icon" href="@PUBLIC_BASE_URL@/static/favicon/favicon.png">
<script src="@PUBLIC_API_URL@/auth/bootstrap.js"></script>

Exemple CSS :

@font-face {
src: url("@PUBLIC_BASE_URL@/static/fonts/nunito-variable.woff2");
}

Exemple JavaScript :

fetch(`@PUBLIC_API_URL@/auth/me`);

Le même token peut apparaître plusieurs fois dans un fichier. Plusieurs tokens différents peuvent également être utilisés.

Utiliser ReplaceTokens avec une syntaxe @TOKEN@ évite les collisions avec :

  • les chaînes JavaScript contenant $
  • les templates JavaScript
  • les expressions CSS
  • les caractères ${...} utilisés ailleurs

expand est moins adapté aux fichiers JavaScript, car il interprète aussi les expressions Groovy $variable et ${expression}.

Les ressources JVM sont généralement placées dans :

src/main/resources/

La tâche standard est :

processResources

Configuration :

import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.language.jvm.tasks.ProcessResources
val publicBaseUrl = providers.environmentVariable("PUBLIC_BASE_URL")
.orElse(providers.gradleProperty("publicBaseUrl"))
val publicApiUrl = providers.environmentVariable("PUBLIC_API_URL")
.orElse(providers.gradleProperty("publicApiUrl"))
tasks.named<ProcessResources>("processResources") {
inputs.property("publicBaseUrl", publicBaseUrl)
inputs.property("publicApiUrl", publicApiUrl)
filteringCharset = "UTF-8"
filesMatching(
listOf(
"static/**/*.html",
"static/**/*.css",
"static/**/*.js",
)
) {
filter(
ReplaceTokens::class,
"tokens" to mapOf(
"PUBLIC_BASE_URL" to publicBaseUrl.get().removeSuffix("/"),
"PUBLIC_API_URL" to publicApiUrl.get().removeSuffix("/"),
),
)
}
}

Les fichiers transformés sont produits dans :

build/resources/main/

La tâche est automatiquement exécutée avant la création du JAR.

Les ressources Web sont généralement placées dans :

src/webMain/resources/

Les cibles Web utilisent des tâches spécifiques. Il n’existe pas de tâche JVM générique processResources.

Pour Wasm :

import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.api.tasks.Copy
val publicBaseUrl = providers.environmentVariable("PUBLIC_BASE_URL")
.orElse(providers.gradleProperty("publicBaseUrl"))
val publicApiUrl = providers.environmentVariable("PUBLIC_API_URL")
.orElse(providers.gradleProperty("publicApiUrl"))
tasks.named<Copy>("wasmJsProcessResources") {
inputs.property("publicBaseUrl", publicBaseUrl)
inputs.property("publicApiUrl", publicApiUrl)
filteringCharset = "UTF-8"
filesMatching(
listOf(
"**/*.html",
"static/**/*.css",
"static/**/*.js",
)
) {
filter(
ReplaceTokens::class,
"tokens" to mapOf(
"PUBLIC_BASE_URL" to publicBaseUrl.get().removeSuffix("/"),
"PUBLIC_API_URL" to publicApiUrl.get().removeSuffix("/"),
),
)
}
}

La distribution est généralement produite avec :

wasmJsBrowserDistribution

Le résultat se trouve généralement dans :

build/dist/wasmJs/productionExecutable/

Pour la cible JavaScript classique, utiliser :

tasks.named<Copy>("jsProcessResources") {
inputs.property("publicBaseUrl", publicBaseUrl)
inputs.property("publicApiUrl", publicApiUrl)
filteringCharset = "UTF-8"
filesMatching(
listOf(
"**/*.html",
"static/**/*.css",
"static/**/*.js",
)
) {
filter(
ReplaceTokens::class,
"tokens" to mapOf(
"PUBLIC_BASE_URL" to publicBaseUrl.get().removeSuffix("/"),
"PUBLIC_API_URL" to publicApiUrl.get().removeSuffix("/"),
),
)
}
}

Un module possédant js et wasmJs doit configurer les deux tâches.

tasks.named<Copy>("jsProcessResources") {
// Configuration JavaScript
}
tasks.named<Copy>("wasmJsProcessResources") {
// Configuration Wasm
}

Une configuration Wasm ne modifie pas automatiquement la cible JavaScript.

Android ne fonctionne pas comme un module JVM classique.

Il faut distinguer deux cas.

Pour un fichier texte ou JSON sous :

src/androidMain/resources/

Le traitement dépend de la configuration Android et de la variante produite. Les tâches peuvent être spécifiques, par exemple :

processDebugJavaRes
processReleaseJavaRes

Il est déconseillé de dépendre directement d’un nom de tâche de variante si plusieurs variantes existent.

Une tâche Copy dédiée est plus stable :

import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.api.tasks.Copy
val publicBaseUrl = providers.environmentVariable("PUBLIC_BASE_URL")
.orElse(providers.gradleProperty("publicBaseUrl"))
val generateAndroidTemplates by tasks.registering(Copy::class) {
from("src/androidMain/resources")
into(layout.buildDirectory.dir("generated/androidResources"))
filteringCharset = "UTF-8"
filesMatching("**/*.json") {
filter(
ReplaceTokens::class,
"tokens" to mapOf(
"PUBLIC_BASE_URL" to publicBaseUrl.get().removeSuffix("/"),
),
)
}
}

Cette sortie doit ensuite être ajoutée aux ressources Android de la variante concernée.

Pour les fichiers sous :

src/androidMain/res/
src/main/res/

Il ne faut pas appliquer un remplacement textuel aveugle à tous les fichiers.

Utiliser plutôt :

  • resValue
  • buildConfigField
  • une ressource XML générée
  • une configuration runtime

Exemple :

android {
defaultConfig {
buildConfigField(
"String",
"PUBLIC_BASE_URL",
"\"https://example.com\"",
)
}
}

Puis dans Kotlin :

val publicBaseUrl = BuildConfig.PUBLIC_BASE_URL

Pour Android, les valeurs de configuration sont souvent plus propres dans BuildConfig ou dans une ressource XML que dans un template HTML filtré.

Les ressources Compose Multiplatform se trouvent généralement dans :

src/commonMain/composeResources/

ou :

src/commonMain/resources/

Elles sont traitées par le plugin Compose Multiplatform et peuvent être générées pour plusieurs plateformes.

Elles conviennent pour :

  • images
  • icônes
  • textes
  • fichiers embarqués
  • ressources localisées

Elles ne sont pas le meilleur endroit pour injecter une URL différente selon l’environnement.

Pour une configuration dépendante de l’environnement, préférer :

expect val publicBaseUrl: String

avec des implémentations spécifiques :

// commonMain
expect val publicBaseUrl: String
// androidMain
actual val publicBaseUrl: String
get() = BuildConfig.PUBLIC_BASE_URL
// wasmJsMain
actual val publicBaseUrl: String
get() = "https://example.com"

Cette approche convient lorsque la valeur doit être connue par le code Kotlin.

Une cible Desktop JVM utilise généralement :

processResources

La configuration est donc proche d’un module JVM classique.

Les cibles Native n’utilisent pas nécessairement un pipeline de ressources identique à JVM.

Pour des fichiers générés par cible, utiliser :

  • une tâche Gradle Copy
  • une tâche Gradle Sync
  • une source générée par cible
  • une configuration runtime

Il faut éviter de supposer qu’une tâche processResources existe pour chaque cible Native.

Le templating Gradle produit un artefact différent pour chaque environnement.

local -> http://localhost:8080
staging -> https://staging.example.com
production -> https://example.com

Avantages :

  • le fichier final est directement utilisable
  • aucune logique supplémentaire au runtime
  • adapté aux fichiers statiques
  • adapté aux URL publiques

Inconvénients :

  • un artefact différent par environnement
  • il faut reconstruire après chaque changement de valeur
  • la valeur doit être connue au moment du build

Si le même artefact doit être déployé partout, utiliser une configuration runtime :

data class WebConfig(
val publicBaseUrl: String,
val publicApiUrl: String,
)

Puis remplacer le template au démarrage ou générer la page depuis Kotlin.

Avec des propriétés Gradle :

Fenêtre de terminal
.\gradlew.bat :src:application:backend:processResources `
-PpublicBaseUrl=http://localhost:8080 `
-PpublicApiUrl=http://localhost:8080

Pour Wasm :

Fenêtre de terminal
.\gradlew.bat `
:src:application:backoffice-app:backoffice-webApp:wasmJsBrowserDistribution `
-PpublicBaseUrl=http://localhost:8080 `
-PpublicApiUrl=http://localhost:8080

Avec des variables d’environnement :

Fenêtre de terminal
$env:PUBLIC_BASE_URL = "http://localhost:8080"
$env:PUBLIC_API_URL = "http://localhost:8080"
.\gradlew.bat `
:src:application:backoffice-app:backoffice-webApp:wasmJsBrowserDistribution

Une variable configurée uniquement dans une tâche de lancement IntelliJ peut ne pas être disponible dans le processus Gradle. Elle doit être configurée dans la tâche Gradle elle-même ou être passée avec -P.

  • Utiliser processResources dans un module Kotlin Multiplatform Web.
  • Utiliser static/html/**/*.html alors que les fichiers sont dans src/webMain/resources.
  • Oublier inputs.property.
  • Utiliser expand sur du JavaScript contenant des expressions $.
  • Filtrer les fichiers générés au lieu des fichiers source.
  • Injecter des secrets dans une ressource Web.
  • Configurer uniquement wasmJsProcessResources alors que la cible construite est js.
  • Modifier src/.../resources directement pendant le build.
  • Ajouter une valeur par défaut de production dangereuse en développement.

Pour ce projet :

  • backend JVM : processResources
  • backoffice Wasm : wasmJsProcessResources
  • backoffice JavaScript : jsProcessResources
  • Android : BuildConfig, resValue ou configuration runtime
  • Compose Multiplatform : ressources Compose pour les fichiers statiques, configuration spécifique par cible pour les URL
  • valeurs publiques : variables d’environnement CI avec fallback dans gradle.properties
  • secrets : jamais dans les templates Web