Skip to content

Commit ab1b930

Browse files
authored
wp_mobile crate (#956)
* Add wp_mobile crate that depends on wp_api * Update Kotlin wrapper to use wp_mobile crate * Make clippy happy by not using foo as an argument name * Address AS duplicate root path issue and desktop compose library loading * Update compose-plugin to 1.9.0 * Generate test credentials file for compose app * Revert an accidental change that regenerates Kotlin bindings for module changes * Adds test credential creation for compose android app to preBuild task * Make test credentials optional for composeapp
1 parent 19fca66 commit ab1b930

File tree

16 files changed

+257
-46
lines changed

16 files changed

+257
-46
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ members = [
1212
"wp_localization_macro",
1313
"wp_localization_parser",
1414
"wp_localization_validation",
15+
"wp_mobile",
1516
"wp_rs_cli",
1617
"wp_rs_web",
1718
"wp_serde_helper",

native/kotlin/api/android/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ dependencies {
8484
}
8585

8686
val cargoProjectRoot = rootProject.ext.get("cargoProjectRoot")!!
87-
val moduleName = "wp_api"
87+
val moduleName = rootProject.ext.get("rustPrimaryModule").toString()
8888
cargo {
8989
cargoCommand = rootProject.ext.get("cargoBinaryPath").toString()
9090
rustcCommand = rootProject.ext.get("rustcBinaryPath").toString()

native/kotlin/api/kotlin/build.gradle.kts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,14 @@ sourceSets {
8484
}
8585
}
8686

87+
// UniFFI supports generating bindings for multiple crates from a single library file.
88+
// When wp_mobile is built, it includes wp_api as a dependency, so libwp_mobile contains
89+
// metadata for both crates. We generate bindings for each crate from the single library.
8790
val generateUniFFIBindingsTask = tasks.register<Exec>("generateUniFFIBindings") {
8891
val cargoProjectRoot = rootProject.ext.get("cargoProjectRoot")
8992
val uniffiGeneratedPath = "${layout.buildDirectory.get()}/generated/source/uniffi/java"
9093
val nativeLibraryPath = rootProject.ext.get("nativeLibraryPath")!!
91-
val rustModuleName = rootProject.ext.get("rustModuleName")
94+
val rustPrimaryModule = rootProject.ext.get("rustPrimaryModule")
9295

9396
dependsOn(rootProject.tasks.named("cargoBuildLibraryRelease"))
9497
workingDir(project.rootDir)
@@ -115,7 +118,7 @@ val generateUniFFIBindingsTask = tasks.register<Exec>("generateUniFFIBindings")
115118
// Re-generate if our uniffi-bindgen version changes.
116119
inputs.file("$cargoProjectRoot/Cargo.lock")
117120
// Re-generate if the module source code changes
118-
inputs.dir("$cargoProjectRoot/$rustModuleName/")
121+
inputs.dir("$cargoProjectRoot/$rustPrimaryModule/")
119122
}
120123

121124
tasks.named("compileKotlin").configure {

native/kotlin/api/kotlin/src/integrationTest/kotlin/UsersEndpointTest.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ import uniffi.wp_api.SparseUserFieldWithEditContext
66
import uniffi.wp_api.UserListParams
77
import uniffi.wp_api.WpApiParamUsersHasPublishedPosts
88
import uniffi.wp_api.WpErrorCode
9+
import uniffi.wp_mobile.wpMobileCrateWorks
910
import kotlin.test.assertEquals
1011
import kotlin.test.assertNull
1112

1213
class UsersEndpointTest {
1314
private val client = defaultApiClient()
1415

16+
@Test
17+
fun testThatWpMobileCrateWorks() = runTest {
18+
assertEquals("foo is bar", wpMobileCrateWorks("bar"))
19+
}
20+
1521
@Test
1622
fun testUserListRequest() = runTest {
1723
val userList = client.request { requestBuilder ->

native/kotlin/build.gradle.kts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ allprojects {
3232
// Exclude generated bindings
3333
exclude("**/wp_api.kt")
3434
exclude("**/wp_localization.kt")
35+
exclude("**/wp_mobile.kt")
3536
}
3637

3738
tasks.withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>().configureEach {
@@ -40,6 +41,7 @@ allprojects {
4041
// Exclude generated bindings
4142
exclude("**/wp_api.kt")
4243
exclude("**/wp_localization.kt")
44+
exclude("**/wp_mobile.kt")
4345
}
4446

4547
dependencies {
@@ -52,28 +54,28 @@ val rustcBinaryPath = resolveBinary("rustc")
5254
val cargoProjectRoot = "${project.rootDir}/../.."
5355
val jniLibsPath = "${layout.buildDirectory.get()}/jniLibs/"
5456
val generatedTestResourcesPath = "${layout.buildDirectory.get()}/generatedTestResources/"
55-
val rustModuleName = "wp_api"
57+
val rustPrimaryModule = "wp_mobile"
5658
val nativeLibraryPath =
57-
"$cargoProjectRoot/target/release/lib${rustModuleName}${getNativeLibraryExtension()}"
59+
"$cargoProjectRoot/target/release/lib${rustPrimaryModule}${getNativeLibraryExtension()}"
5860

5961
rootProject.ext.set("cargoBinaryPath", cargoBinaryPath)
6062
rootProject.ext.set("rustcBinaryPath", rustcBinaryPath)
6163
rootProject.ext.set("cargoProjectRoot", cargoProjectRoot)
6264
rootProject.ext.set("jniLibsPath", jniLibsPath)
6365
rootProject.ext.set("generatedTestResourcesPath", generatedTestResourcesPath)
6466
rootProject.ext.set("nativeLibraryPath", nativeLibraryPath)
65-
rootProject.ext.set("rustModuleName", rustModuleName)
67+
rootProject.ext.set("rustPrimaryModule", rustPrimaryModule)
6668

6769
setupJniAndBindings()
6870

6971
// Separated as a function to have everything in a scope and keep it contained
7072
fun setupJniAndBindings() {
7173
val nativeLibraryPath =
72-
"$cargoProjectRoot/target/release/lib${rustModuleName}${getNativeLibraryExtension()}"
74+
"$cargoProjectRoot/target/release/lib${rustPrimaryModule}${getNativeLibraryExtension()}"
7375

7476
val cargoBuildLibraryReleaseTask = tasks.register<Exec>("cargoBuildLibraryRelease") {
7577
workingDir(rootProject.ext.get("cargoProjectRoot")!!)
76-
commandLine(cargoBinaryPath, "build", "--package", rustModuleName, "--release")
78+
commandLine(cargoBinaryPath, "build", "--package", rustPrimaryModule, "--release")
7779
// No inputs.dir added, because we want to always re-run this task and let Cargo handle caching
7880
}
7981

native/kotlin/example/composeApp/build.gradle.kts

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,25 @@ tasks.withType<KotlinJvmCompile>().configureEach {
1818
}
1919
}
2020

21+
// Desktop resources path - separate from integration test resources to avoid IDE conflicts
22+
val desktopResourcesPath = layout.buildDirectory.dir("desktopResources")
23+
24+
// Copy resources needed for desktop app
25+
val copyDesktopAppResources = tasks.register<Copy>("copyDesktopAppResources") {
26+
dependsOn(rootProject.tasks.named("copyDesktopJniLibs"))
27+
dependsOn(rootProject.tasks.named("copyTestCredentials"))
28+
from(rootProject.ext.get("jniLibsPath"))
29+
from(rootProject.ext.get("generatedTestResourcesPath"))
30+
into(desktopResourcesPath)
31+
}
32+
2133
kotlin {
2234
androidTarget()
2335
jvm("desktop")
2436

2537
sourceSets {
2638
val desktopMain by getting {
27-
resources.srcDirs(
28-
listOf(
29-
rootProject.ext.get("jniLibsPath"),
30-
rootProject.ext.get("generatedTestResourcesPath")
31-
)
32-
)
39+
resources.srcDirs(desktopResourcesPath)
3340
}
3441

3542
androidMain.dependencies {
@@ -71,7 +78,6 @@ android {
7178

7279
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
7380
sourceSets["main"].res.srcDirs("src/androidMain/res")
74-
sourceSets["main"].resources.srcDirs("src/commonMain/resources")
7581

7682
defaultConfig {
7783
applicationId = "rs.wordpress.example"
@@ -106,6 +112,11 @@ compose.desktop {
106112
application {
107113
mainClass = "rs.wordpress.example.MainKt"
108114

115+
jvmArgs += listOf(
116+
"-Djna.library.path=.",
117+
"-Djava.library.path=."
118+
)
119+
109120
nativeDistributions {
110121
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
111122
packageName = "rs.wordpress.example"
@@ -114,7 +125,87 @@ compose.desktop {
114125
}
115126
}
116127

128+
// Generate BuildConfig with rust module name
129+
val generateBuildConfig = tasks.register("generateBuildConfig") {
130+
val outputDir = layout.buildDirectory.dir("generated/source/buildConfig")
131+
val rustPrimaryModule = rootProject.ext.get("rustPrimaryModule") as String
132+
133+
outputs.dir(outputDir)
134+
135+
doLast {
136+
val buildConfigFile = outputDir.get().file("rs/wordpress/example/BuildConfig.kt").asFile
137+
buildConfigFile.parentFile.mkdirs()
138+
buildConfigFile.writeText("""
139+
package rs.wordpress.example
140+
141+
object BuildConfig {
142+
const val RUST_PRIMARY_MODULE = "$rustPrimaryModule"
143+
}
144+
""".trimIndent())
145+
}
146+
}
147+
148+
// Generate TestCredentials from test_credentials.json
149+
val generateTestCredentials = tasks.register("generateTestCredentials") {
150+
val outputDir = layout.buildDirectory.dir("generated/source/testCredentials")
151+
val cargoProjectRoot = rootProject.ext.get("cargoProjectRoot") as String
152+
val credentialsFile = file("$cargoProjectRoot/test_credentials.json")
153+
154+
// Only mark as input if file exists - allows build to work without test server running
155+
inputs.files(credentialsFile).optional(true)
156+
outputs.dir(outputDir)
157+
158+
doLast {
159+
val testCredentialsFile = outputDir.get().file("rs/wordpress/example/TestCredentials.kt").asFile
160+
testCredentialsFile.parentFile.mkdirs()
161+
162+
if (credentialsFile.exists()) {
163+
val json = groovy.json.JsonSlurper().parseText(credentialsFile.readText()) as Map<*, *>
164+
testCredentialsFile.writeText("""
165+
package rs.wordpress.example
166+
167+
object TestCredentials {
168+
val SITE_URL: String? = "${json["site_url"]}"
169+
val ADMIN_USERNAME: String? = "${json["admin_username"]}"
170+
val ADMIN_PASSWORD: String? = "${json["admin_password"]}"
171+
val SUBSCRIBER_USERNAME: String? = "${json["subscriber_username"]}"
172+
val SUBSCRIBER_PASSWORD: String? = "${json["subscriber_password"]}"
173+
}
174+
""".trimIndent())
175+
} else {
176+
testCredentialsFile.writeText("""
177+
package rs.wordpress.example
178+
179+
object TestCredentials {
180+
val SITE_URL: String? = null
181+
val ADMIN_USERNAME: String? = null
182+
val ADMIN_PASSWORD: String? = null
183+
val SUBSCRIBER_USERNAME: String? = null
184+
val SUBSCRIBER_PASSWORD: String? = null
185+
}
186+
""".trimIndent())
187+
}
188+
}
189+
}
190+
191+
kotlin.sourceSets.getByName("desktopMain") {
192+
kotlin.srcDir(layout.buildDirectory.dir("generated/source/buildConfig"))
193+
}
194+
195+
kotlin.sourceSets.getByName("commonMain") {
196+
kotlin.srcDir(layout.buildDirectory.dir("generated/source/testCredentials"))
197+
}
198+
199+
tasks.named("compileKotlinDesktop").configure {
200+
dependsOn(generateBuildConfig)
201+
dependsOn(generateTestCredentials)
202+
}
203+
204+
// Ensure test credentials are generated before any Android compilation
205+
tasks.named("preBuild").configure {
206+
dependsOn(generateTestCredentials)
207+
}
208+
117209
tasks.named("desktopProcessResources").configure {
118-
dependsOn(rootProject.tasks.named("copyDesktopJniLibs"))
119-
dependsOn(rootProject.tasks.named("copyTestCredentials"))
210+
dependsOn(copyDesktopAppResources)
120211
}

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/di/AppModule.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package rs.wordpress.example.shared.di
22

33
import org.koin.dsl.module
4+
import rs.wordpress.example.TestCredentials
45
import rs.wordpress.example.shared.localTestSiteUrl
56
import rs.wordpress.example.shared.repository.AuthenticationRepository
67
import rs.wordpress.example.shared.ui.plugins.PluginListViewModel
@@ -9,15 +10,14 @@ import rs.wordpress.example.shared.ui.welcome.WelcomeViewModel
910

1011
val authModule = module {
1112
single {
12-
// TODO: Read from test credentials file
1313
AuthenticationRepository(
1414
localTestSiteUrl = localTestSiteUrl().siteUrl,
15-
localTestSiteUsername = "[email protected]",
16-
// Until this works with the included test credentials, you can grab it from the
17-
// `test_credentials.json` file `make test-server` will generate in the root of the repo
18-
// The key is `admin_password`
19-
localTestSitePassword = "s3N7vlbdrFPDDI3MbyFUvS3P"
20-
)
15+
localTestSiteUsername = TestCredentials.ADMIN_USERNAME,
16+
localTestSitePassword = TestCredentials.ADMIN_PASSWORD
17+
).apply {
18+
// Add test site if credentials are available
19+
addTestSiteIfAvailable()
20+
}
2121
}
2222
}
2323

native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/repository/AuthenticationRepository.kt

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@ package rs.wordpress.example.shared.repository
33
import rs.wordpress.example.shared.domain.AuthenticatedSite
44
import uniffi.wp_api.WpAuthentication
55
import uniffi.wp_api.wpAuthenticationFromUsernameAndPassword
6+
import java.net.URI
67
import java.net.URL
78

89
class AuthenticationRepository(
9-
localTestSiteUrl: String,
10-
localTestSiteUsername: String,
11-
localTestSitePassword: String
10+
private val localTestSiteUrl: String,
11+
private val localTestSiteUsername: String?,
12+
private val localTestSitePassword: String?
1213
) {
1314
private val authenticatedSites = mutableMapOf<AuthenticatedSite, WpAuthentication>()
1415

15-
init {
16-
addAuthenticatedSite(
17-
URL(localTestSiteUrl),
18-
URL("$localTestSiteUrl/wp-json"),
19-
localTestSiteUsername,
20-
localTestSitePassword
21-
)
16+
fun addTestSiteIfAvailable() {
17+
if (localTestSiteUsername != null && localTestSitePassword != null) {
18+
addAuthenticatedSite(
19+
URI(localTestSiteUrl).toURL(),
20+
URI("$localTestSiteUrl/wp-json").toURL(),
21+
localTestSiteUsername,
22+
localTestSitePassword
23+
)
24+
}
2225
}
2326

2427
fun addAuthenticatedSite(siteUrl: URL, apiRootUrl: URL, username: String, password: String): Boolean {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package rs.wordpress.example
2+
3+
import java.io.File
4+
import java.nio.file.Files
5+
6+
object NativeLibraryLoader {
7+
private val tempDir = Files.createTempDirectory("wordpress-rs-libs").toFile().apply {
8+
deleteOnExit()
9+
}
10+
11+
fun loadLibraries() {
12+
// Determine the library name based on the OS
13+
val osName = System.getProperty("os.name").lowercase()
14+
val moduleName = BuildConfig.RUST_PRIMARY_MODULE
15+
val libName = when {
16+
osName.contains("mac") || osName.contains("darwin") -> "lib${moduleName}.dylib"
17+
osName.contains("linux") -> "lib${moduleName}.so"
18+
osName.contains("windows") -> "${moduleName}.dll"
19+
else -> throw UnsupportedOperationException("Unsupported OS: $osName")
20+
}
21+
22+
// Extract the native library from jar
23+
try {
24+
val resourceStream = javaClass.classLoader.getResourceAsStream(libName)
25+
if (resourceStream != null) {
26+
val tempFile = File(tempDir, libName).apply {
27+
deleteOnExit()
28+
}
29+
30+
resourceStream.use { input ->
31+
tempFile.outputStream().use { output ->
32+
input.copyTo(output)
33+
}
34+
}
35+
36+
// Make executable
37+
tempFile.setExecutable(true)
38+
println("Extracted native library: ${tempFile.absolutePath}")
39+
} else {
40+
println("Warning: Could not find $libName in resources")
41+
}
42+
} catch (e: Exception) {
43+
println("Warning: Could not extract $libName: ${e.message}")
44+
}
45+
46+
// Set JNA library path to our temp directory
47+
System.setProperty("jna.library.path", tempDir.absolutePath)
48+
System.setProperty("java.library.path", tempDir.absolutePath)
49+
50+
println("Native library path set to: ${tempDir.absolutePath}")
51+
}
52+
}

0 commit comments

Comments
 (0)