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
5 changes: 5 additions & 0 deletions .changeset/legacy-manifest-namespace-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@callstack/react-native-brownfield': patch
---

fix: resolve the namespace from AndroidManifest.xml `package` for libraries without a `namespace` in build.gradle
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ jobs:
- name: Build packages
run: yarn build

- name: Publish Brownfield Gradle Plugin to Maven Local
run: yarn run brownfield:plugin:publish:local

- name: Scaffold a fresh RN CLI app and assemble the packaging module
run: bash ./scripts/ci-scaffold-smoke-android.sh

Expand Down
2 changes: 1 addition & 1 deletion apps/RNApp/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ buildscript {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha10-SNAPSHOT")
classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha11-SNAPSHOT")
}
}

Expand Down
15 changes: 7 additions & 8 deletions apps/scripts/prepare-android-build-gradle-for-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ if (!projectDirName) {

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const SNAPSHOT_VERSION = '2.0.0-alpha10-SNAPSHOT';
const targetPath = path.resolve(
__dirname,
'..',
projectDirName,
'android',
'build.gradle'
);
const SNAPSHOT_VERSION = '2.0.0-alpha11-SNAPSHOT';
// Accepts a repo-relative app dir (e.g. "ExpoApp58") or an absolute path (e.g.
// the scaffold-smoke app created outside the repo).
const projectDir = path.isAbsolute(projectDirName)
? projectDirName
: path.resolve(__dirname, '..', projectDirName);
const targetPath = path.resolve(projectDir, 'android', 'build.gradle');

function ensureMavenLocalInBuildscriptRepositories(contents: string): string {
const buildscriptReposRegex =
Expand Down
2 changes: 1 addition & 1 deletion gradle-plugins/react/brownfield/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PROJECT_ID=com.callstack.react.brownfield
ARTIFACT_ID=brownfield-gradle-plugin
VERSION=2.0.0-alpha10
VERSION=2.0.0-alpha11
GROUP=com.callstack.react
IMPLEMENTATION_CLASS=com.callstack.react.brownfield.plugin.RNBrownfieldPlugin

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.callstack.react.brownfield.utils
import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
import org.gradle.api.Project
import java.io.File
import javax.xml.parsers.DocumentBuilderFactory

class AndroidArchiveLibrary(
private val project: Project,
Expand Down Expand Up @@ -46,11 +47,47 @@ class AndroidArchiveLibrary(
val regex = Regex("""namespace\s*=?\s*["']([^"']+)["']""")
val match = regex.find(text)

val namespace =
match?.groupValues?.get(1)
?: error("No namespace found in ${buildFile.path}")
return match?.groupValues?.get(1)
?: getNameSpaceFromAndroidManifest(subProj)
Comment thread
hurali97 marked this conversation as resolved.
?: error(
"No namespace found in ${buildFile.path}, and no package attribute found in " +
"AndroidManifest.xml for ${artifact.moduleName}",
)
}

return namespace
/**
* Falls back to the `package` attribute of the dependency's `AndroidManifest.xml` when the
* module declares no `namespace` in its `build.gradle`. The Android Gradle plugin itself
* derives the namespace this way for such libraries, so they build fine as a regular
* dependency but trip up [getNameSpaceFromBuildGradle]'s `build.gradle`-only regex.
*
* The manifest is parsed as XML and only the root `<manifest>` element's `package`
* attribute is read, so comments or child attributes such as `android:targetPackage`
* cannot be mistaken for the package name.
*/
private fun getNameSpaceFromAndroidManifest(subProj: Project): String? {
val candidateManifests =
listOf(
File(subProj.projectDir, "src/main/AndroidManifest.xml"),
File(subProj.projectDir, "AndroidManifest.xml"),
)

return candidateManifests
.filter { it.exists() }
.firstNotNullOfOrNull { manifestFile ->
runCatching {
val document =
DocumentBuilderFactory.newInstance().apply {
isNamespaceAware = true
setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
setFeature("http://xml.org/sax/features/external-general-entities", false)
setFeature("http://xml.org/sax/features/external-parameter-entities", false)
}.newDocumentBuilder().parse(manifestFile)
document.documentElement
?.getAttribute("package")
?.takeIf { it.isNotBlank() }
}.getOrNull()
}
}

fun getManifestFile() = File(getExplodedAarRootDir(), "AndroidManifest.xml")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.callstack.react.brownfield.utils

import com.callstack.react.brownfield.shared.UnresolvedArtifactInfo
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class AndroidArchiveLibraryNamespaceFallbackTest {
private fun artifactFor(moduleName: String) =
UnresolvedArtifactInfo(
moduleGroup = "com.example",
moduleName = moduleName,
moduleVersion = "1.0.0",
file = null,
isExpoPublishDependency = false,
)

@Test
fun `falls back to the AndroidManifest package when build-gradle declares no namespace`() {
val root = ProjectBuilder.builder().build()
val subProject =
ProjectBuilder.builder()
.withParent(root)
.withName("react-native-launch-arguments")
.build()

// No `namespace` declaration, mirroring libraries whose build.gradle predates AGP namespaces.
subProject.projectDir.mkdirs()
subProject.buildFile.writeText(
"""
apply plugin: 'com.android.library'
""".trimIndent(),
)
subProject.projectDir.resolve("src/main").mkdirs()
subProject.projectDir.resolve("src/main/AndroidManifest.xml").writeText(
"""
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactnativelauncharguments">
</manifest>
""".trimIndent(),
)

val library = AndroidArchiveLibrary(root, artifactFor("react-native-launch-arguments"), "devRelease")

assertEquals("com.reactnativelauncharguments", library.getPackageName())
}

@Test
fun `prefers the namespace declared in build-gradle over the manifest package`() {
val root = ProjectBuilder.builder().build()
val subProject =
ProjectBuilder.builder()
.withParent(root)
.withName("react-native-screens")
.build()

subProject.projectDir.mkdirs()
subProject.buildFile.writeText(
"""
apply plugin: 'com.android.library'
android {
namespace = "com.swmansion.rnscreens"
}
""".trimIndent(),
)
subProject.projectDir.resolve("src/main").mkdirs()
subProject.projectDir.resolve("src/main/AndroidManifest.xml").writeText(
"""
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.swmansion.rnscreens.legacy">
</manifest>
""".trimIndent(),
)

val library = AndroidArchiveLibrary(root, artifactFor("react-native-screens"), "devRelease")

assertEquals("com.swmansion.rnscreens", library.getPackageName())
}

@Test
fun `reads the package from the manifest root element and ignores comments and child attributes`() {
val root = ProjectBuilder.builder().build()
val subProject =
ProjectBuilder.builder()
.withParent(root)
.withName("react-native-legacy-manifest")
.build()

subProject.projectDir.mkdirs()
subProject.buildFile.writeText(
"""
apply plugin: 'com.android.library'
""".trimIndent(),
)
subProject.projectDir.resolve("src/main").mkdirs()
subProject.projectDir.resolve("src/main/AndroidManifest.xml").writeText(
"""
<!-- legacy location: package="com.example.stale" -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactnativelegacymanifest">
<application>
<activity
android:name=".LegacyActivity"
android:exported="false"
android:targetPackage="com.example.target" />
</application>
</manifest>
""".trimIndent(),
)

val library = AndroidArchiveLibrary(root, artifactFor("react-native-legacy-manifest"), "devRelease")

assertEquals("com.reactnativelegacymanifest", library.getPackageName())
}

@Test
fun `errors when neither build-gradle nor the manifest declare a namespace`() {
val root = ProjectBuilder.builder().build()
val subProject =
ProjectBuilder.builder()
.withParent(root)
.withName("no-namespace-anywhere")
.build()

subProject.projectDir.mkdirs()
subProject.buildFile.writeText(
"""
apply plugin: 'com.android.library'
""".trimIndent(),
)

val library = AndroidArchiveLibrary(root, artifactFor("no-namespace-anywhere"), "devRelease")

assertFailsWith<IllegalStateException> { library.getPackageName() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('gradleHelpers', () => {
}`;

expect(modifyRootBuildGradle(contents)).toContain(
'classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha10")'
'classpath("com.callstack.react:brownfield-gradle-plugin:2.0.0-alpha11")'
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const BROWNFIELD_PLUGIN_VERSION = '2.0.0-alpha10';
export const BROWNFIELD_PLUGIN_VERSION = '2.0.0-alpha11';
export const brownfieldGradlePluginDependency = `classpath("com.callstack.react:brownfield-gradle-plugin:${BROWNFIELD_PLUGIN_VERSION}")`;
11 changes: 11 additions & 0 deletions scripts/ci-scaffold-smoke-android.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ grep -qF "brownfield-gradle-plugin:$PLUGIN_VERSION" "$GRADLE_DIR/build.gradle" |
grep -q "include ':brownfieldlib'" "$GRADLE_DIR/settings.gradle" || { echo "settings.gradle lacks ':brownfieldlib'"; exit 1; }
echo "::endgroup::"

echo "::group::Patch root build.gradle for CI snapshot plugin"
# The Gradle plugin under test is not published to Maven Central yet. Like the
# road-test CI, resolve it from Maven Local as <VERSION>-SNAPSHOT (the previous
# step publishes it with `yarn brownfield:plugin:publish:local`). The script
# also adds mavenLocal() to the buildscript repositories.
node --experimental-strip-types --no-warnings \
"$REPO_ROOT/apps/scripts/prepare-android-build-gradle-for-ci.ts" "$WORKDIR/$APP_NAME"
grep -q "mavenLocal()" "$GRADLE_DIR/build.gradle" || { echo "root build.gradle lacks mavenLocal()"; exit 1; }
grep -q "brownfield-gradle-plugin:.*-SNAPSHOT" "$GRADLE_DIR/build.gradle" || { echo "root build.gradle lacks the SNAPSHOT Gradle plugin classpath"; exit 1; }
echo "::endgroup::"

echo "::group::Assemble Android packaging module"
# The freshly generated app pins its own ndkVersion in buildscript ext; install
# it if missing (CI runners carry a different preinstalled set). No-op locally
Expand Down
Loading