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
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"wp_localization_macro",
"wp_localization_parser",
"wp_localization_validation",
"wp_mobile",
"wp_rs_cli",
"wp_rs_web",
"wp_serde_helper",
Expand Down
2 changes: 1 addition & 1 deletion native/kotlin/api/android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ dependencies {
}

val cargoProjectRoot = rootProject.ext.get("cargoProjectRoot")!!
val moduleName = "wp_api"
val moduleName = rootProject.ext.get("rustPrimaryModule").toString()
cargo {
cargoCommand = rootProject.ext.get("cargoBinaryPath").toString()
rustcCommand = rootProject.ext.get("rustcBinaryPath").toString()
Expand Down
7 changes: 5 additions & 2 deletions native/kotlin/api/kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,14 @@ sourceSets {
}
}

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

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

tasks.named("compileKotlin").configure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ import uniffi.wp_api.SparseUserFieldWithEditContext
import uniffi.wp_api.UserListParams
import uniffi.wp_api.WpApiParamUsersHasPublishedPosts
import uniffi.wp_api.WpErrorCode
import uniffi.wp_mobile.wpMobileCrateWorks
import kotlin.test.assertEquals
import kotlin.test.assertNull

class UsersEndpointTest {
private val client = defaultApiClient()

@Test
fun testThatWpMobileCrateWorks() = runTest {
assertEquals("foo is bar", wpMobileCrateWorks("bar"))
}

@Test
fun testUserListRequest() = runTest {
val userList = client.request { requestBuilder ->
Expand Down
12 changes: 7 additions & 5 deletions native/kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ allprojects {
// Exclude generated bindings
exclude("**/wp_api.kt")
exclude("**/wp_localization.kt")
exclude("**/wp_mobile.kt")
}

tasks.withType<io.gitlab.arturbosch.detekt.DetektCreateBaselineTask>().configureEach {
Expand All @@ -40,6 +41,7 @@ allprojects {
// Exclude generated bindings
exclude("**/wp_api.kt")
exclude("**/wp_localization.kt")
exclude("**/wp_mobile.kt")
}

dependencies {
Expand All @@ -52,28 +54,28 @@ val rustcBinaryPath = resolveBinary("rustc")
val cargoProjectRoot = "${project.rootDir}/../.."
val jniLibsPath = "${layout.buildDirectory.get()}/jniLibs/"
val generatedTestResourcesPath = "${layout.buildDirectory.get()}/generatedTestResources/"
val rustModuleName = "wp_api"
val rustPrimaryModule = "wp_mobile"
val nativeLibraryPath =
"$cargoProjectRoot/target/release/lib${rustModuleName}${getNativeLibraryExtension()}"
"$cargoProjectRoot/target/release/lib${rustPrimaryModule}${getNativeLibraryExtension()}"

rootProject.ext.set("cargoBinaryPath", cargoBinaryPath)
rootProject.ext.set("rustcBinaryPath", rustcBinaryPath)
rootProject.ext.set("cargoProjectRoot", cargoProjectRoot)
rootProject.ext.set("jniLibsPath", jniLibsPath)
rootProject.ext.set("generatedTestResourcesPath", generatedTestResourcesPath)
rootProject.ext.set("nativeLibraryPath", nativeLibraryPath)
rootProject.ext.set("rustModuleName", rustModuleName)
rootProject.ext.set("rustPrimaryModule", rustPrimaryModule)

setupJniAndBindings()

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

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

Expand Down
109 changes: 100 additions & 9 deletions native/kotlin/example/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,25 @@ tasks.withType<KotlinJvmCompile>().configureEach {
}
}

// Desktop resources path - separate from integration test resources to avoid IDE conflicts
val desktopResourcesPath = layout.buildDirectory.dir("desktopResources")

// Copy resources needed for desktop app
val copyDesktopAppResources = tasks.register<Copy>("copyDesktopAppResources") {
dependsOn(rootProject.tasks.named("copyDesktopJniLibs"))
dependsOn(rootProject.tasks.named("copyTestCredentials"))
from(rootProject.ext.get("jniLibsPath"))
from(rootProject.ext.get("generatedTestResourcesPath"))
into(desktopResourcesPath)
}

kotlin {
androidTarget()
jvm("desktop")

sourceSets {
val desktopMain by getting {
resources.srcDirs(
listOf(
rootProject.ext.get("jniLibsPath"),
rootProject.ext.get("generatedTestResourcesPath")
)
)
resources.srcDirs(desktopResourcesPath)
}

androidMain.dependencies {
Expand Down Expand Up @@ -71,7 +78,6 @@ android {

sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
sourceSets["main"].res.srcDirs("src/androidMain/res")
sourceSets["main"].resources.srcDirs("src/commonMain/resources")

defaultConfig {
applicationId = "rs.wordpress.example"
Expand Down Expand Up @@ -106,6 +112,11 @@ compose.desktop {
application {
mainClass = "rs.wordpress.example.MainKt"

jvmArgs += listOf(
"-Djna.library.path=.",
"-Djava.library.path=."
)

nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "rs.wordpress.example"
Expand All @@ -114,7 +125,87 @@ compose.desktop {
}
}

// Generate BuildConfig with rust module name
val generateBuildConfig = tasks.register("generateBuildConfig") {
val outputDir = layout.buildDirectory.dir("generated/source/buildConfig")
val rustPrimaryModule = rootProject.ext.get("rustPrimaryModule") as String

outputs.dir(outputDir)

doLast {
val buildConfigFile = outputDir.get().file("rs/wordpress/example/BuildConfig.kt").asFile
buildConfigFile.parentFile.mkdirs()
buildConfigFile.writeText("""
package rs.wordpress.example

object BuildConfig {
const val RUST_PRIMARY_MODULE = "$rustPrimaryModule"
}
""".trimIndent())
}
}

// Generate TestCredentials from test_credentials.json
val generateTestCredentials = tasks.register("generateTestCredentials") {
val outputDir = layout.buildDirectory.dir("generated/source/testCredentials")
val cargoProjectRoot = rootProject.ext.get("cargoProjectRoot") as String
val credentialsFile = file("$cargoProjectRoot/test_credentials.json")

// Only mark as input if file exists - allows build to work without test server running
inputs.files(credentialsFile).optional(true)
outputs.dir(outputDir)

doLast {
val testCredentialsFile = outputDir.get().file("rs/wordpress/example/TestCredentials.kt").asFile
testCredentialsFile.parentFile.mkdirs()

if (credentialsFile.exists()) {
val json = groovy.json.JsonSlurper().parseText(credentialsFile.readText()) as Map<*, *>
testCredentialsFile.writeText("""
package rs.wordpress.example

object TestCredentials {
val SITE_URL: String? = "${json["site_url"]}"
val ADMIN_USERNAME: String? = "${json["admin_username"]}"
val ADMIN_PASSWORD: String? = "${json["admin_password"]}"
val SUBSCRIBER_USERNAME: String? = "${json["subscriber_username"]}"
val SUBSCRIBER_PASSWORD: String? = "${json["subscriber_password"]}"
}
""".trimIndent())
} else {
testCredentialsFile.writeText("""
package rs.wordpress.example

object TestCredentials {
val SITE_URL: String? = null
val ADMIN_USERNAME: String? = null
val ADMIN_PASSWORD: String? = null
val SUBSCRIBER_USERNAME: String? = null
val SUBSCRIBER_PASSWORD: String? = null
}
""".trimIndent())
}
}
}

kotlin.sourceSets.getByName("desktopMain") {
kotlin.srcDir(layout.buildDirectory.dir("generated/source/buildConfig"))
}

kotlin.sourceSets.getByName("commonMain") {
kotlin.srcDir(layout.buildDirectory.dir("generated/source/testCredentials"))
}

tasks.named("compileKotlinDesktop").configure {
dependsOn(generateBuildConfig)
dependsOn(generateTestCredentials)
}

// Ensure test credentials are generated before any Android compilation
tasks.named("preBuild").configure {
dependsOn(generateTestCredentials)
}

tasks.named("desktopProcessResources").configure {
dependsOn(rootProject.tasks.named("copyDesktopJniLibs"))
dependsOn(rootProject.tasks.named("copyTestCredentials"))
dependsOn(copyDesktopAppResources)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rs.wordpress.example.shared.di

import org.koin.dsl.module
import rs.wordpress.example.TestCredentials
import rs.wordpress.example.shared.localTestSiteUrl
import rs.wordpress.example.shared.repository.AuthenticationRepository
import rs.wordpress.example.shared.ui.plugins.PluginListViewModel
Expand All @@ -9,15 +10,14 @@ import rs.wordpress.example.shared.ui.welcome.WelcomeViewModel

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@ package rs.wordpress.example.shared.repository
import rs.wordpress.example.shared.domain.AuthenticatedSite
import uniffi.wp_api.WpAuthentication
import uniffi.wp_api.wpAuthenticationFromUsernameAndPassword
import java.net.URI
import java.net.URL

class AuthenticationRepository(
localTestSiteUrl: String,
localTestSiteUsername: String,
localTestSitePassword: String
private val localTestSiteUrl: String,
private val localTestSiteUsername: String?,
private val localTestSitePassword: String?
) {
private val authenticatedSites = mutableMapOf<AuthenticatedSite, WpAuthentication>()

init {
addAuthenticatedSite(
URL(localTestSiteUrl),
URL("$localTestSiteUrl/wp-json"),
localTestSiteUsername,
localTestSitePassword
)
fun addTestSiteIfAvailable() {
if (localTestSiteUsername != null && localTestSitePassword != null) {
addAuthenticatedSite(
URI(localTestSiteUrl).toURL(),
URI("$localTestSiteUrl/wp-json").toURL(),
localTestSiteUsername,
localTestSitePassword
)
}
}

fun addAuthenticatedSite(siteUrl: URL, apiRootUrl: URL, username: String, password: String): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package rs.wordpress.example

import java.io.File
import java.nio.file.Files

object NativeLibraryLoader {
private val tempDir = Files.createTempDirectory("wordpress-rs-libs").toFile().apply {
deleteOnExit()
}

fun loadLibraries() {
// Determine the library name based on the OS
val osName = System.getProperty("os.name").lowercase()
val moduleName = BuildConfig.RUST_PRIMARY_MODULE
val libName = when {
osName.contains("mac") || osName.contains("darwin") -> "lib${moduleName}.dylib"
osName.contains("linux") -> "lib${moduleName}.so"
osName.contains("windows") -> "${moduleName}.dll"
else -> throw UnsupportedOperationException("Unsupported OS: $osName")
}

// Extract the native library from jar
try {
val resourceStream = javaClass.classLoader.getResourceAsStream(libName)
if (resourceStream != null) {
val tempFile = File(tempDir, libName).apply {
deleteOnExit()
}

resourceStream.use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
}
}

// Make executable
tempFile.setExecutable(true)
println("Extracted native library: ${tempFile.absolutePath}")
} else {
println("Warning: Could not find $libName in resources")
}
} catch (e: Exception) {
println("Warning: Could not extract $libName: ${e.message}")
}

// Set JNA library path to our temp directory
System.setProperty("jna.library.path", tempDir.absolutePath)
System.setProperty("java.library.path", tempDir.absolutePath)

println("Native library path set to: ${tempDir.absolutePath}")
}
}
Loading