BuildKonfig icon indicating copy to clipboard operation
BuildKonfig copied to clipboard

add an easy way to load flavor from arbitrary properties file

Open yshrsmz opened this issue 5 years ago β€’ 8 comments

def configureBuildKonfigFlavorFromLocalProperties() {
    if (project.gradle.startParameter.projectProperties.containsKey("buildkonfig.flavor")) {
        // prefer cli parameter
        return
    }
    if (Files.exists(Paths.get("$project.rootDir/local.properties"))) {
        def key = "buildkonfig.flavor"
        // load buildkonfig.flavor if exists
        def localProperties = new Properties()
        localProperties.load(new FileInputStream("$project.rootDir/local.properties"))
        if (localProperties.containsKey(key)) {
            project.setProperty(key, localProperties.getProperty(key))
        }
    }
}

personally I wrote this script and use it in my project. But it would be nice if this is baked into BuildKonfig

yshrsmz avatar Jan 07 '20 16:01 yshrsmz

@yshrsmz hi, thank you for the great tool. I'm quite new to Kotlin in general, where do you put this script so it will get the flavor from local.properties instead of gradle.properties?

yukai18 avatar Dec 16 '21 04:12 yukai18

@yukai18 Write it in a build.gradle which you apply BuildKonfig plugin, and call it.

apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'com.android.library'
apply plugin: 'com.codingfeline.buildkonfig' // <- BuildKonfig plugin

// call it
configureBuildKonfigFlavorFromLocalProperties()

kotlin {
  // kotlin configuration
}

buildkonfig {
  // BuildKonfig configuration
}

def configureBuildKonfigFlavorFromLocalProperties() {
  // THE method
}

Please note this is written in groovy, so if you are using KTS then you need to re-write this in Kotlin

yshrsmz avatar Dec 16 '21 04:12 yshrsmz

@yshrsmz thank you for your fast response, I tried converting it to Kotlin, the sync was successful, i run ./gradlew :shared:generateBuildKonfig in the terminal, it was successful as well but no file was generated. Can you kindly help check if I am doing something wrong? Here is my code in build.gradle.kts located inside shared module:

import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING

plugins {
    id("com.codingfeline.buildkonfig") version "0.11.0" // BuilKonfig plugin
}

configureBuildKonfigFlavorFromLocalProperties()

kotlin {
  // kotlin config
}

buildkonfig {
    packageName = "com.example.packageName"
    defaultConfigs {
        buildConfigField(STRING, "flavor", "prod")
    }
    defaultConfigs("debug") {
        buildConfigField(STRING, "flavor", "debug")
    }
}

fun configureBuildKonfigFlavorFromLocalProperties() {
    val key = "buildkonfig.flavor"
    if (project.gradle.startParameter.projectProperties.containsKey(key)) {
        // prefer cli parameter
        return
    }
    // load buildkonfig.flavor if exists
    val localProperties = gradleLocalProperties(rootDir)
    if (localProperties.containsKey(key)) {
        project.setProperty(key, localProperties.getProperty(key))
    }
}

in my local.properties file

buildkonfig.flavor="debug"

I had to set below code in my gradle.properties that is located at the project's root directory or else it will generate could not set unkown type 'buildkonfig.flavor' error when running project.setProperty(key, localProperties.getProperty(key))

#BuildKonfig
buildkonfig.flavor=

my local.properties and gradle.properties files are at the project root directory, while the build.gradle.kts is inside shared module, not sure if this will matter but I did confirm that localProperties.containsKey(key) was able to get the key from local.properties

yukai18 avatar Dec 16 '21 08:12 yukai18

@yukai18 Can you provide a minimal repro?

buildkonfig setup looks fine, but I'd like to know how you configure the entire project(or at least shared module)

yshrsmz avatar Dec 16 '21 08:12 yshrsmz

Hi @yshrsmz , as I was creating the mini repro project, I realized my mistakes. I'm able to fully integrate the plugin now. Thank you so much! Sorry for the trouble.

Here's what I did, in case others are experiencing the same issue as mine For the generated file not showing, make sure that you did not exclude the build folder in the project navigator, I forgot if this is excluded by default or I somehow turned it off before. πŸ˜… Screen Shot 2021-12-17 at 12 13 25

For the local.properties file make sure there are no double quotes on the string, I wasn't able to make the flavor work because of this buildkonfig.flavor=debug

yukai18 avatar Dec 17 '21 04:12 yukai18

hi @yshrsmz, it seems that I cannot get your script working. I have a root project (ProjectA) which include another project called ProjectB and is a Kotlin multiplatform library project and BuildKonfig was included in ProjectB.

However, no matter what I set in ProjectA's local.properties, it just simply won't recognize and BuildKonfig will just use the default flavor. I can only change the flavor in ProjectA's gradle.properties. Do you have any insight on what should I look for? Thanks! :)

raymondctc avatar Apr 21 '22 11:04 raymondctc

@raymondctc

I'm not sure if ProjectB is a module or a project, but I think you can use rootProject instead

if (Files.exists(Paths.get("$rootProject.rootDir/local.properties"))) {

or simply add ..

if (Files.exists(Paths.get("$project.rootDir/../local.properties"))) {

(I didn't test these snippets, but you can get the idea)

yshrsmz avatar Apr 21 '22 12:04 yshrsmz

@raymondctc

I'm not sure if ProjectB is a module or a project, but I think you can use rootProject instead

if (Files.exists(Paths.get("$rootProject.rootDir/local.properties"))) {

or simply add ..

if (Files.exists(Paths.get("$project.rootDir/../local.properties"))) {

(I didn't test these snippets, but you can get the idea)

Thanks! I finally pulled the code and figured out the reason. My project structure is a bit complicated and looks like this

MyProject/
β”œβ”€β”€ BuildScriptProject
β”‚Β Β  β”œβ”€β”€ gradle.properties
β”‚Β Β  └── gradlew
β”œβ”€β”€ KMMProjects
β”‚Β Β  └── ProjectB
└── ProjectA
    └── build.gradle

By printing project.projectDir and project.rootProject.projectDir on ProjectA's build.gradle, the following result is given:

projectDir=/Users/me/MyProject/ProjectA
rootProjectDir=/Users/me/MyProject/BuildScriptProject

And when I try to print project.projectDirand project.rootProject.projectDir in BuildKonfigPlugin's configure call, the following result is given

projectDir=/Users/me/MyProject/ProjectB
rootProjectDir=/Users/me/MyProject/BuildScriptProject

And I learnt that findProperty will by default check for ProjectB's gradle.properties, if it doesn't exist, it will check for the root project (BuildScriptProject). I tried to do the following in ProjectA:

projectDir.rootProjectDir.setProperty(key, flavor)

It doesn't work. findProperty won't search anything to the root project that's set progrmmatically, it only reads value from gradle.properties of the root project.

Solution

  1. Create a gradle.properties in ProjectB, add key buildkonfig.flavor=
  2. In ProjectA's build.gradle
project(':ProjectB').setProperty(key, flavor)

Now it works!

I made a modification to your script too with automatic flavor detection. In case some one is interested:

def configureBuildKonfigFlavorFromLocalProperties() {
	def key = "buildkonfig.flavor"
    if (project.gradle.startParameter.projectProperties.containsKey(key)) {
        // prefer cli parameter
        return
    }

    def targetProject = project(":ProjectB")
    def currFlavor = getCurrentFlavor()
    targetProject.setProperty(key, currFlavor)
}

def getCurrentFlavor() {
    Gradle gradle = getGradle()
    String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()

    Pattern pattern

    if (tskReqStr.contains("assemble") || tskReqStr.contains("install")) {
        pattern = Pattern.compile("(assemble|install)(\\w+)(Release|Debug|Jokes)")
    } else {
        pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
    }

    Matcher matcher = pattern.matcher( tskReqStr )

    if( matcher.find() ) {
        return matcher.group(3).toLowerCase()
    } else {
        println "NO MATCH FOUND"
        return ""
    }
}

raymondctc avatar Apr 22 '22 02:04 raymondctc

Hello! First of all I want to say you thank you for such a great library! I've used snippets from previous posts to fix one flaw of BuildKonfig: it doesn't create, nor uses flavors from Android app module. It would be useful if library used flavor from assemble task and somehow passes it to generateBuildKonfig task. I want to try to write such functionality, either for public usage or for gaining personal expertise. Can you tell me which classes of BuildKonfig should be changed to achieve this, or why it wasn't written or shouldn't be written?

Dzmitry-Lakisau avatar Oct 24 '23 23:10 Dzmitry-Lakisau

@Dzmitry-Lakisau

Hi,

Flavor(or build variant) in Android is a fairly complex feature.

https://developer.android.com/build/build-variants

You can define multiple flavor dimensions, and I'm not sure how we can distinguish which flavor relates to BuildKonfig's flavor.

Here's a groovy script from my previous project, which extracts Android flavor from the task name and maps it to BuildKonfig's flavor.

My project had a single flavor dimension env, so this snippet is relatively simple. But if you have multiple flavor dimensions… Β―\(ツ)/Β―

def configureBuildKonfigFlavorFromAndroidTasks() {
    if (project.gradle.startParameter.projectProperties.containsKey("buildkonfig.flavor")) {
        // prefer cli parameter
        println("buildkonfig.flavor=${project.gradle.startParameter.projectProperties["buildkonfig.flavor"]}")
        return
    }

    def pattern = "^:androidApp:(assemble|test|bundle|extractApksFor)(\\w+)(Release|Debug)(|UnitTest)\$"
    def runningTasks = project.gradle.startParameter.taskNames
    def matchingTask = runningTasks.find { it.matches(pattern) }
    if (matchingTask == null) {
        return
    }

    Pattern p = Pattern.compile(pattern)
    Matcher m = p.matcher(matchingTask)

    if (!m.find()) {
        return
    }

    def flavor = m.group(2)
    def buildkonfigFlavor = ""
    // map matching android flavor to BuildKonfig's flavor
    switch (flavor) {
        case "Dev":
            buildkonfigFlavor = "dev"
            break
        case "Stg":
            buildkonfigFlavor = "stg"
            break
        case "Prd":
            def buildType = m.group(3)
            if (buildType == "Debug") {
                buildkonfigFlavor = "prdDebug"
            } else {
                buildkonfigFlavor = "prd"
            }
            break
    }

    println("presentation-base:buildkonfig.flavor=${buildkonfigFlavor}")

    project.setProperty("buildkonfig.flavor", buildkonfigFlavor)
}

yshrsmz avatar Oct 25 '23 02:10 yshrsmz

@yshrsmz Above solution only works best for android targets but for iOS target it is not able to find flavour.

Here is code.

//gradle.startParameter.taskRequests.toString() gives
[DefaultTaskExecutionRequest{args=[:shared:embedAndSignAppleFrameworkForXcode],projectPath='null',rootDir='null'}]

Which does not contain any Release or Debug buildTypes, eventually it is not able to find any flavour. Can you please suggest any solutions or workaround?

Do i need to define any flavours in xcode?

farhazulmullick-pw avatar Nov 21 '23 08:11 farhazulmullick-pw

@farhazulmullick-pw you can configure BuildKonfig flavor by passing -P parameter to gradlew command. This should work for Xcode project too.

./gradlew build -Pbuildkonfig.flavor=release

https://github.com/yshrsmz/BuildKonfig#product-flavor

yshrsmz avatar Nov 21 '23 08:11 yshrsmz

@farhazulmullick-pw you can configure BuildKonfig flavor by passing -P parameter to gradlew command. This should work for Xcode project too.

./gradlew build -Pbuildkonfig.flavor=release

https://github.com/yshrsmz/BuildKonfig#product-flavor

So @yshrsmz we cannot dynamically get productFlavours for iOS targets from AndoridTarget?

farhazulmullick-pw avatar Nov 21 '23 09:11 farhazulmullick-pw

@farhazulmullick-pw Yes because Android targets have nothing to do with iOS targets

yshrsmz avatar Nov 21 '23 09:11 yshrsmz

@yshrsmz Ok. So, would I have to define explicitly define targetConfigs() for devDebug, devRelease, stageDebug, stageRelease, prodDebug, prodRelease to make work with iOS?

Because earlier i was doing someting like this and it was working flawless on andorid.

kotlin {
    applyDefaultHierarchyTemplate()
    buildConfigs = getCurrentBuildConfigs()
    project.setProperty(FLAVOR_PROPERTY, buildConfigs.flavour)
    ....
}

fun getCurrentBuildConfigs(): BuildConfigs {
    val taskRequestsStr = gradle.startParameter.taskRequests.toString()
    .....
    // calulate and fill BuildConfig
    return buildConfigs
}

data class BuildConfigs(
    val buildType: String = "",
    val flavour: String = "",
    val buildVarient: String = ""
)

buildkonfig {
    packageName = "com.penpencil.parent"

    defaultConfigs {
        buildConfigField(STRING, "BUILD_TYPE", buildConfigs.buildType, nullable = true)
        buildConfigField(STRING, "FLAVOUR", buildConfigs.flavour, nullable = true)
        buildConfigField(STRING, "BUILD_VARIENT", buildConfigs.buildVarient, nullable = true)
    }

    targetConfigs("dev") {
        create("android") {}
        create("ios") {}
    }
    targetConfigs("prod") {
        create("android") {}
        create("ios") {}
    }

}

farhazulmullick-pw avatar Nov 21 '23 09:11 farhazulmullick-pw

@farhazulmullick-pw

So, would I have to define explicitly define targetConfigs() for devDebug, devRelease, stageDebug, stageRelease, prodDebug, prodRelease to make work with iOS?

From the point of view of the BuildKonfig author, if you need to distinguish between prodDebug and prodRelease, then yes, you need to define these flavors.

But if you really want to go that way(which is not the BuildKonfig-standard), you should be able to simply pass some arbitrary property and do whatever you want.

// this code is executed from Xcode
./gradlew :shared:embedAndSignAppleFrameworkForXcode -Pyourcustomflavor=prdDebug
fun getCurrentBuildConfigs(): BuildConfigs {
    val taskRequestsStr = gradle.startParameter.taskRequests.toString()
    
    // check if yourcustomflavor exists
    val iOSflavor = project.findProperty("yourcustomflavor")

    if (iOSflavor != null) {
      // calculate config for IOS
    }
    .....
    // calulate and fill BuildConfig
    return buildConfigs
}

yshrsmz avatar Nov 21 '23 09:11 yshrsmz

@farhazulmullick-pw

So, would I have to define explicitly define targetConfigs() for devDebug, devRelease, stageDebug, stageRelease, prodDebug, prodRelease to make work with iOS?

From the point of view of the BuildKonfig author, if you need to distinguish between prodDebug and prodRelease, then yes, you need to define these flavors.

But if you really want to go that way(which is not the BuildKonfig-standard), you should be able to simply pass some arbitrary property and do whatever you want.

// this code is executed from Xcode
./gradlew :shared:embedAndSignAppleFrameworkForXcode -Pyourcustomflavor=prdDebug
fun getCurrentBuildConfigs(): BuildConfigs {
    val taskRequestsStr = gradle.startParameter.taskRequests.toString()
    
    // check if yourcustomflavor exists
    val iOSflavor = project.findProperty("yourcustomflavor")

    if (iOSflavor != null) {
      // calculate config for IOS
    }
    .....
    // calulate and fill BuildConfig
    return buildConfigs
}

Thanks @yshrsmz Its is a great help.

farhazulmullick-pw avatar Nov 21 '23 11:11 farhazulmullick-pw

Do i need to define at BuildPhases

Just update your existing Xcode configuration(should be in BuildPhase though)

Also I did not defined iOSFlavour at gradle.properties

That's depending on your preference. If you want default value, then define it.

yshrsmz avatar Nov 21 '23 11:11 yshrsmz

Thank You @yshrsmz for your help :)

farhazulmullick-pw avatar Nov 22 '23 05:11 farhazulmullick-pw

@yshrsmz We have added these scripts to generate a build for iOS unfortunately its not working, can you help us to know what's wrong in these scripts


cd "$SRCROOT"
./gradlew :shared:embedAndSignAppleFrameworkForXcode -Pios.flavor=prodDebug
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"

the last one we use for Crashlytics

val iOSFlavor = project.findProperty("ios.flavor")
this value gives us null for iOS but in Android works fine

sureshmaidaragi1919 avatar Dec 19 '23 06:12 sureshmaidaragi1919

@sureshmaidaragi1919 Can you provide a relevant part of your build.gradle script?

yshrsmz avatar Dec 19 '23 07:12 yshrsmz

@sureshmaidaragi1919 Can you provide a relevant part of your build.gradle script?

here it is inside build.gradle(shared)


apply(from = "$rootDir/gradle/scripts/buildVarients.gradle")

var buildConfigs = BuildConfigs()

kotlin {
    buildConfigs = getCurrentBuildConfigs()
    project.setProperty(FLAVOR_PROPERTY, buildConfigs.flavour) //this will set current build config default as staging


....
.,...

fun getCurrentBuildConfigs(): BuildConfigs {
    val iOSFlavor = project.findProperty("ios.flavor")

 **//iOSFlavor  value is always null, is the issue which shouldn't be**
 

  

    if (iOSFlavor != null) {
        return iOSBuildConfigs(iOSFlavor)
    }
    val taskRequestsStr = gradle.startParameter.taskRequests.toString()
    val pattern: Pattern = if (taskRequestsStr.contains("assemble")) {
        Pattern.compile("assemble(\\w+)(Release|Debug)")
    } else {
        Pattern.compile("bundle(\\w+)(Release|Debug)")
    }
    val matcher = pattern.matcher(taskRequestsStr)
    val buildConfigs = if (matcher.find()) {
        val flavour = matcher.group(1).lowercase()
        val buildType = matcher.group(2)
        BuildConfigs(
            flavour = flavour,
            buildType = buildType.lowercase(),
            buildVarient = "${flavour}$buildType"
        )
    } else {
        println("No android product-flavour found!")
        BuildConfigs()
    }
    return buildConfigs
}

data class BuildConfigs(
    val buildType: String = "",
    val flavour: String = "",
    val buildVarient: String = ""
)


sureshmaidaragi1919 avatar Dec 19 '23 07:12 sureshmaidaragi1919

Again, that is a pure Gradle problem and I can't exactly help with it(and I don't recommend that configuration).

But can you check what happens if you use project.gradle.startParameter.projectProperties["ios.flavor"] like I do in this comment?

https://github.com/yshrsmz/BuildKonfig/issues/23#issuecomment-1778373687

or rootProject.findProperty(...) instead?

yshrsmz avatar Dec 19 '23 08:12 yshrsmz

Tried both still didnt help

project.gradle.startParameter.projectProperties["ios.flavor"]

rootProject.findProperty("ios.flavor")

sureshmaidaragi1919 avatar Dec 19 '23 10:12 sureshmaidaragi1919

That's weird πŸ€” Could you create a minimal reproducible project?

I updated ./sample-kts/build.gradle.kts with your snippet and ran a following command.

./gradlew -p sample-kts compileKotlinIosArm64 -Pios.flavor=test

and it successfully found ios.flavor property.

So it should be something to do with your gradle configuration or xcode-specific tasks such as embedAndSignAppleFrameworkForXcode

yshrsmz avatar Dec 19 '23 10:12 yshrsmz