Create Free/Paid versions of Application from same code - android

So I'm coming down to release-time for my application. We plan on releasing two versions, a free ad-based play-to-unlock version, and a paid fully unlocked version. I have the code set up that I can simply set a flag on startup to enable/disable ads and lock/unlock all the features. So literally only one line of code will execute differently between these versions.
In order to release two separate applications, they require different package names, so my question is this: Is there an easy way to refactor my application's package name? Eclipse's refactoring tool doesn't resolve the generated R file, or any XML references in layout and manifest files. I've attempted to make a new project using the original as source, but I can't reference the assets and resources, and I'm looking to avoid duplicating any of my code and assets. It's not a huge pain to refactor it manually, but I feel there must be a better way to do it. Anybody have an elegant solution to this?
Edit/Answered:
For my situation I find it perfectly acceptable to just use Project -> Android Tools -> Rename Application Package. I wasn't aware this existed, and I feel like an idiot for posting this now. Thanks for everyone's answers and comments, feel free to vote this closed.

It's very simple by using build.gradle in Android Studio. Read about productFlavors. It is a very usefull feature. Just simply add following lines in build.gradle:
productFlavors {
lite {
packageName = 'com.project.test.app'
versionCode 1
versionName '1.0.0'
}
pro {
packageName = 'com.project.testpro.app'
versionCode 1
versionName '1.0.0'
}
}
In this example I add two product flavors: first for lite version and second for full version. Each version has his own versionCode and versionName (for Google Play publication).
In code just check BuildConfig.FLAVOR:
if (BuildConfig.FLAVOR == "lite") {
// add some ads or restrict functionallity
}
For running and testing on device use "Build Variants" tab in Android Studio to switch between versions:

Possibly a duplicate of Bulk Publishing of Android Apps.
Android Library projects will do this for you nicely. You'll end up with 1 library project and then a project for each edition (free/full) with those really just containing different resources like app icons and different manifests, which is where the package name will be varied.
Hope that helps. It has worked well for me.

The best way is to use "Android Studio" -> gradle.build -> [productFlavors + generate manifest file from template]. This combination allows to build free/paid versions and bunch of editions for different app markets from one source.
This is a part of templated manifest file:
<manifest android:versionCode="1" android:versionName="1" package="com.example.product" xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="#drawable/ic_launcher"
android:label="#string/{f:FREE}app_name_free{/f}{f:PAID}app_name_paid{/f}"
android:name=".ApplicationMain" android:theme="#style/AppTheme">
<activity android:label="#string/{f:FREE}app_name_free{/f}{f:PAID}app_name_paid{/f}" android:name=".ActivityMain">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
This is template "ProductInfo.template" for java file: ProductInfo.java
package com.packagename.generated;
import com.packagename.R;
public class ProductInfo {
public static final boolean mIsPaidVersion = {f:PAID}true{/f}{f:FREE}false{/f};
public static final int mAppNameId = R.string.app_name_{f:PAID}paid{/f}{f:FREE}free{/f};
public static final boolean mIsDebug = {$DEBUG};
}
This manifest is processed by gradle.build script with productFlavors and processManifest task hook:
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
...
android {
...
productFlavors {
free {
packageName 'com.example.product.free'
}
paid {
packageName 'com.example.product.paid'
}
}
...
}
afterEvaluate { project ->
android.applicationVariants.each { variant ->
def flavor = variant.productFlavors[0].name
tasks['prepare' + variant.name + 'Dependencies'].doLast {
println "Generate java files..."
//Copy templated and processed by build system manifest file to filtered_manifests forder
def productInfoPath = "${projectDir}/some_sourcs_path/generated/"
copy {
from(productInfoPath)
into(productInfoPath)
include('ProductInfo.template')
rename('ProductInfo.template', 'ProductInfo.java')
}
tasks.create(name: variant.name + 'ProcessProductInfoJavaFile', type: processTemplateFile) {
templateFilePath = productInfoPath + "ProductInfo.java"
flavorName = flavor
buildTypeName = variant.buildType.name
}
tasks[variant.name + 'ProcessProductInfoJavaFile'].execute()
}
variant.processManifest.doLast {
println "Customization manifest file..."
// Copy templated and processed by build system manifest file to filtered_manifests forder
copy {
from("${buildDir}/manifests") {
include "${variant.dirName}/AndroidManifest.xml"
}
into("${buildDir}/filtered_manifests")
}
tasks.create(name: variant.name + 'ProcessManifestFile', type: processTemplateFile) {
templateFilePath = "${buildDir}/filtered_manifests/${variant.dirName}/AndroidManifest.xml"
flavorName = flavor
buildTypeName = variant.buildType.name
}
tasks[variant.name + 'ProcessManifestFile'].execute()
}
variant.processResources.manifestFile = file("${buildDir}/filtered_manifests/${variant.dirName}/AndroidManifest.xml")
}
}
This is separated task to process file
class processTemplateFile extends DefaultTask {
def String templateFilePath = ""
def String flavorName = ""
def String buildTypeName = ""
#TaskAction
void run() {
println templateFilePath
// Load file to memory
def fileObj = project.file(templateFilePath)
def content = fileObj.getText()
// Flavor. Find "{f:<flavor_name>}...{/f}" pattern and leave only "<flavor_name>==flavor"
def patternAttribute = Pattern.compile("\\{f:((?!${flavorName.toUpperCase()})).*?\\{/f\\}",Pattern.DOTALL);
content = patternAttribute.matcher(content).replaceAll("");
def pattern = Pattern.compile("\\{f:.*?\\}");
content = pattern.matcher(content).replaceAll("");
pattern = Pattern.compile("\\{/f\\}");
content = pattern.matcher(content).replaceAll("");
// Build. Find "{$DEBUG}" pattern and replace with "true"/"false"
pattern = Pattern.compile("\\{\\\$DEBUG\\}", Pattern.DOTALL);
if (buildTypeName == "debug"){
content = pattern.matcher(content).replaceAll("true");
}
else{
content = pattern.matcher(content).replaceAll("false");
}
// Save processed manifest file
fileObj.write(content)
}
}
Updated: processTemplateFile created for code reusing purposes.

Gradle allows to use generated BuildConfig.java to pass some data to code.
productFlavors {
paid {
packageName "com.simple.paid"
buildConfigField 'boolean', 'PAID', 'true'
buildConfigField "int", "THING_ONE", "1"
}
free {
packageName "com.simple.free"
buildConfigField 'boolean', 'PAID', 'false'
buildConfigField "int", "THING_ONE", "0"
}

For everyone who want to use the solution by Denis:
In the new gradle version packageName is now applicationId and don't forget to put productFlavors { ... } in android { ... }
productFlavors {
lite {
applicationId = 'com.project.test.app'
versionCode 1
versionName '1.0.0'
}
pro {
applicationId = 'com.project.testpro.app'
versionCode 1
versionName '1.0.0'
}
}

One approach I'm experimenting with is using fully-qualified names for activities, and just changing the package attribute. It avoids any real refactoring (1 file copy, 1 text sub).
This almost works, but the generated R class isn't picked up, as the package for this is pulled out of AndroidManifest.xml, so ends up in the new package.
I think it should be fairly straight forward to build AndroidManifest.xml via an Ant rule (in -pre-build) that inserts the distribution package name, and then (in -pre-compile) the generated resources into the default (Java) package.
Hope this helps,
Phil Lello

If you want another application name, depending of the flavor, you can also add this:
productFlavors {
lite {
applicationId = 'com.project.test.app'
resValue "string", "app_name", "test lite"
versionCode 1
versionName '1.0.0'
}
pro {
applicationId = 'com.project.testpro.app'
resValue "string", "app_name", "test pro"
versionCode 1
versionName '1.0.0'
}
}

Related

Create and access productFlavor variables in android's build.gradle

I've a multi flavor project which is built by a CI and published to HockeyApp.
Each flavor has an applicationId and an apiToken, which is stored in the flavor itself (to keep all important variables in one place):
def token = null
productFlavors {
prod {
applicationId "de.example.appname"
buildConfigField 'String', 'FLAVOR_ID', '"0"'
buildConfigField 'String', 'HOCKEY_APP_ID', '"1234567890"'
token = "1q2w3e4r5t6z7u8i9o0p"
}
demo {
applicationId "de.example.appname.demo"
buildConfigField 'String', 'FLAVOR_ID', '"1"'
buildConfigField 'String', 'HOCKEY_APP_ID', '"987654321"'
token = "p0o9i8u7z6t5r4e3w2q1"
}
}
On the same level like "productFlavors" there are the hockeyApp-settings:
hockeyapp {
apiToken = token
releaseType = 0
notify = 0
status = 1
notesType = 1
notes = "Uploaded with gradle"
}
For debugging the code I build & upload the .apk-file via terminal:
./gradlew uploadProdReleaseToHockeyApp [...]
Unfortunately the variable token of the prod-flavor is always overridden by the demo-value. So after each uploading process I get errors like
Error response from HockeyApp: App could not be created.
because gradle tries to upload the prod-flavor with the demo-token.
Here some additional basic data:
compileSdkVersion 24
buildToolsVersion "24.0.1"
compile 'net.hockeyapp.android:HockeySDK:4.0.0'
classpath 'com.android.tools.build:gradle:2.1.3'
classpath 'de.felixschulze.gradle:gradle-hockeyapp-plugin:3.5'
Based on my requirements, is there a solution to define flavor-variables and access them in the shown way?
In this special case I found following answer:
Add the hockeyapp-task with your modifications needed
hockeyapp {
apiToken = "not_required"
releaseType = 0
notify = 0
status = 2
teams = 1234
notesType = 1
}
In the next step add flavor-based gradle tasks to modify you hockeyapp's apiToken:
task setDevReleaseApiToken << {
hockeyapp.apiToken = "1234567890abcdefghijklmnopqrstuvwxyz"
}
task setProdReleaseApiToken << {
hockeyapp.apiToken = "1234567890abcdefghijklmnopqrstuvwxyz"
}
These tasks are called in gradle's whenTaskAdded-task, you can simply "override" it like this:
tasks.whenTaskAdded { task ->
if (task.name == 'uploadDevReleaseToHockeyApp') {
task.dependsOn 'setDevReleaseApiToken'
} else if (task.name == 'uploadProdReleaseToHockeyApp') {
task.dependsOn 'setProdReleaseApiToken'
}
}
Everytime the task uploadDevReleaseToHockeyApp is called (manually or by CI..) the task setDevReleaseApiToken is called and the related apiToken is assigned.
Extend this schema for all other flavors if needed!
Though the OP might not be looking for an answer anymore, someone other might stumble upon this thread (like I did when I was dealing with this problem) so I'd like to share a solution.
I was dealing with variables having effect on gradle procedure itself (not on the source code) so I couldn't use resValue or buildConfigField.
This problem can be solved by using the settings.gradle file. This file executes before build.gradle, so you can prepare all your variables using this condition:
include ':project...'
rootProject.name = 'name...'
def VARIANT1 = "YourFlavorName";
if((gradle.startParameter.taskNames.contains(":assemble"+VARIANT1+"Debug"))||
(gradle.startParameter.taskNames.contains(":assemble"+VARIANT1+"Release"))
||
(gradle.startParameter.taskNames.contains(":generate"+VARIANT1+"DebugSources"))||(gradle.startParameter.taskNames.contains(":generate"+VARIANT1+"ReleaseSources"))) {
gradle.ext.variable = value
gradle.ext.var...
}
It is a bit of a work-around but what this does is it checks the build commands given by your Android Studio and searches for the flavor name inside. Conditions with ":generate" are just for Building, ":assebmle" is for Running the application on a device.
Any variable you create in your settings file like this can be accessed from your build.gradle file.
myVariable = gradle.ext.variable
This should do it.

Localizing string resources added via build.gradle using "resValue"

This is in continuation to an answer which helped me on this post
We can add the string resource as follows from build.gradle:
productFlavors {
main{
resValue "string", "app_name", "InTouch Messenger"
}
googlePlay{
resValue "string", "app_name", "InTouch Messenger: GPE Edition"
}
}
It works like a charm and serves the purpose of having different app names per flavor. (with the original app_name string resource deleted from strings.xml file.
But, how do we add localized strings for this string resource added from build.gradle ?
Is there an additional parameter we can pass specifying the locale?
OR
Possible to do it using a gradle task?
Note: I cannot do this using strings.xml (not feasible because of several ways in which my project is structured)
My other answer about the generated resources may be an overkill for you use case though. Base what I currently know about your project I think this one is a better fit:
(not that you can still combine this with generated resources)
src/flavor1/res/values/strings.xml
<string name="app_name_base">InTouch Messenger"</string>
<string name="app_name_gpe">InTouch Messenger: GPE Edition"</string>
src/flavor1/res/values-hu/strings.xml
<string name="app_name_base">InTouch Üzenetküldő"</string>
<string name="app_name_gpe">InTouch Üzenetküldő: GPE Változat"</string>
src/flavor2/res/values/strings.xml
<string name="app_name_base">Whatever Messenger"</string>
<string name="app_name_gpe">Whatever Messenger: GPE Edition"</string>
src/flavor2/res/values-hu/strings.xml`
<string name="app_name_base">Whatever Üzenetküldő"</string>
<string name="app_name_gpe">Whatever Üzenetküldő: GPE Változat"</string>
build.gradle
android {
sourceSets {
[flavor1, flavor3].each {
it.res.srcDirs = ['src/flavor1/res']
}
[flavor2, flavor4].each {
it.res.srcDirs = ['src/flavor2/res']
}
}
productFlavors { // notice the different numbers than sourceSets
[flavor1, flavor2].each {
it.resValue "string", "app_name", "#string/app_name_base"
}
[flavor3, flavor4].each {
it.resValue "string", "app_name", "#string/app_name_gpe"
}
}
}
This means that flavor1/2 will have an extra unused app_name_gpe string resource, but that'll be taken care of by aapt:
android {
buildTypes {
release {
shrinkResources true // http://tools.android.com/tech-docs/new-build-system/resource-shrinking
}
If you do not have to operate on those strings, the best option is moving to strings.xml, but that would make you share all res folder between flavors.
If you generate these strings based on some property on build.gradle, then I think you're out of luck, unfortunately.
EDIT: clarifying what I mean by operate above and add some options:
By operating on those strings I mean some sort of concatenation with a build parameter, a reading from command line or environment variable during the build process (e.g., getting the commit SHA1 so that it's easier to trace bugs later). If no operation is necessary, strings.xml may be an option. But when you overwrite a res folder for flavor, all of it is overwritten and that could pose a problem if several flavors share the same res except for a limited number of strings.
If each APK has its own locale, then it's just a resValue or buildConfigField in a flavor. You can define variables to for easier reuse of values. Something like
def myVar = "var"
...
flavor1 {
resValue "string", "my_res_string", "${myVar}"
}
flavor2 {
resValue "string", "my_res_string", "${myVar}"
}
But if several locales are needed in the same APK and it will be chosen at runtime by Android, then the string must be in the correct values-<locale> folder.
You're operating on different levels here, BuildConfig is code, and as such not localized, that's why we have Lint warnings for hard-coded strings. Localization in Android is done via <string resources, there's no way around that if you want the system to choose the language at runtime depending on user settings. There are many ways to have resources though: values folder, resValue in build.gradle, and generated resources.
You should look into the buildSrc project in Gradle, for example I use it to generate SQL Inserts from src/main/values/stuff.xml. Here's some code to start with.
buildSrc/build.gradle
// To enable developing buildSrc in IDEA import buildSrc/build.gradle as a separate project
// Create a settings.gradle in buildSrc as well to prevent importing as subproject
apply plugin: 'groovy'
repositories { jcenter() }
dependencies {
compile localGroovy()
compile gradleApi()
testCompile 'junit:junit:4.12'
}
buildSrc/src/main/groovy/Plugin.groovy
import org.gradle.api.*
/**
* Use it as
* <code>
* apply plugin: MyPlugin
* myEntities {
* categories {
* input = file(path to Android res xml with Strings)
* output = file(path to asset SQL file)
* conversion = "structure|SQL"
* }
* }
* </code>
*/
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
def entities = project.container(MyEntity)
// this gives the name for the block in build.gradle
project.extensions.myEntities = entities
def allTasks = project.task('generateYourStuff')
def allTasksClean = project.task('cleanGenerateYourStuff')
project.afterEvaluate {
entities.all { entity ->
//println "Creating task for ${entity.name} (${entity.input} --${entity.conversion}--> ${entity.output})"
def task = project.task(type: GenerateTask, "generateYourStuff${entity.name.capitalize()}") {
input = entity.input
output = entity.output
conversion = entity.conversion
}
allTasks.dependsOn task
// clean task is automagically generated for every task that has output
allTasksClean.dependsOn "clean${task.name.capitalize()}"
}
}
}
}
class MyEntity {
def input
def output
String conversion
final String name
MyEntity(String name) {
this.name = name
}
}
buildSrc/src/main/groovy/GenerateTask.groovy
import net.twisterrob.inventory.database.*
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.*
class GenerateTask extends DefaultTask {
#InputFile File input
#OutputFile File output
#Optional #Input String conversion
#TaskAction void generate() {
input.withReader { reader ->
// you may need to treat output as a folder
output.parentFile.mkdirs()
output.withWriter { writer ->
// custom transformation here read from reader, write to writer
}
}
}
}
This is just the skeleton you can go wild and do anything from here: e.g. retrieve a CSV through the network and spread the contents into generated variant*/res/values-*/gen.xml files.
You can run it manually when you need to or run it at the right point in the build lifecycle (in build.gradle:
android.applicationVariants.all { com.android.build.gradle.api.ApplicationVariant variant ->
variant.mergeAssets.dependsOn tasks.generateYourStuff
}

Intellij - gradle refresh for android project that requires a parameter

I have gradle project, the simplified android part of the build.gradle of which looks like this:
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 22
versionCode 1
versionName "1.0"
def mapPlacesApiKeys = [dev_1: [api_key: 'key_1', sender_id: 'sender_id_1'], dev_9: [api_key: 'key_2', sender_id: 'sender_id_2'],prod_1: [api_key: 'key_3', sender_id: 'sender_id_3']]
if (project.hasProperty('env')) {
if (mapPlacesApiKeys.get(env) == null) {
def keyset = mapPlacesApiKeys.keySet()
throw new StopExecutionException("Value '$env' is not a valid environment value. Valid environments: $keyset. You can set an environment by passing the -Penv=<env> parameter to your gradle build.")
}
}
buildConfigField "String", "SENDER_ID", "\"${mapPlacesApiKeys.get(env).get('sender_id')}\""
manifestPlaceholders = [maps_places_api_key: mapPlacesApiKeys.get(env).get('api_key')]
}
}
When I run this from the command line I run it with an environment parameter e.g. -Penv=dev_1. This does 2 things as you can see in the script:
a) Replaces in the androidmanifest the value "maps_places_api_key" with the actual key of the specified environment
b) Adds a String with the SENDER_ID in the BuildConfig java class.
This works fine from the command line. My problem is that in intellij (or android studio, should be the same) when I import the gradle project I get the following error:
-Penv is missing. You can set an environment by passing the -Penv=<env> parameter to your gradle build.
Which is the exception that I am declaring. So my question is how can I structure this code so that the IDE doesn't try to run it when it's loading the project.
Note that I don't want to replace the "env" parameter with any kinds of flavors or buildTypes, as there's about 15 different environments and it's gonna be a mess, but if there's another suggestion that comes in to your mind feel free to share it.
android{
defaultConfig {
applicationId 'com.xx'
minSdkVersion 14
}
productFlavors {
def path="./channel.txt"
file(path).eachLine { line->
def words = line.split(':')
def name = words[0]
def sender_id = words[1]
def api_key = words[2]
"$name" {
buildConfigField "String", "SENDER_ID", sender_id
manifestPlaceholders = [maps_places_api_key: api_key]
}
}
}
}

Android Gradle identify current falvor at compile time

How is it possible to identify the current flavor being compiled. I'm trying to add a file to compile only if I'm compiling a certain product flavor.
buildTypes {
android.applicationVariants.all { variant ->
variant.productFlavors.each() { flavor ->
if (flavor.name.equals(currentFlavorName)) {
The problem is that I can't seem to find where the currentFlavourName of the flavor which I am currently building is located.
just put the strings you want for flavor1 into:
src/flavor1/res/values/strings.xml
and the strings for flavor2 into:
src/flavor2/res/values/strings.xml
no need to put logic into your gradle file
Android uses a unique build process regarding your resources for different flavors and it is very easy to control.
if you set up your main source:
project-name
------------/app
---------------/src
-------------------/main
------------------------/res
----------------------------/values
------------------------/java
-------------------/development
-------------------------------/res
-----------------------------------/values
-------------------------------/java
-------------------/production
------------------------------/res
----------------------------------/values
------------------------------/java
This would be a bottom up approach from product flavor into main. Meaning if you have a strings.xml with items having the same name existing in development/res/values and have values that also exist in main/res/values/strings.xml these will be over written (and same would go for the production flavor) based on the build variant defined in your gradle file.
android {
productFlavors {
production {
applicationId "com.test.prod"
versionName "1.0.0"
}
development {
applicationId "com.test.dev"
versionName "1.0.0"
}
}
I don't know if exits a method to get the currentFlavor. I haven't found it yet.
A ugly solution can be
variant.productFlavors.each() { flavor ->
if (flavor.name.equals("flavor1")) {
//..........
}
}
However, if you want to be able to control which strings.xml you are using, you can achieve it in different ways.
First of all you can just define a xml file in your flavor folder.
app/src/main/res/values/ -> for common resources
app/src/flavor1/res/values -> resources for flavor1
app/src/flavor2/res/values -> resources for flavor2
This doesn't require any config in your build.gradle script.
A second option is to define a resource value using build.gradle.
Something like:
productFlavors {
flavor1 {
resValue "string", "app_name", "IRCEnterprise"
}
//....
}
Another option is to create some field in your BuildConfig class using this kind of script.
productFlavors {
flavor1 {
buildConfigField "String", "name", "\"MY FLAVOR NAME\""
}
}

Change generated apk name from "app-debug.apk"

Every time I run a project with Android Studio (1.02) it's generate an unsigned apk which located in ..\build\outputs\apk folder and get by default the name "app-debug.apk"
I want to change that default name to something else.
Is it possible? How?
In build.gradle (Module: app):
android {
...
defaultConfig {
...
setProperty("archivesBaseName", "MyNewAppNameGoesHere")
}
}
This works by modifying the archivesBaseName-property and works for Gradle Version >= 2.0, last tested with 2.1.0.
You can use applicationVariants and change the output file in the build.gradle. You can also modify the name regarding to your needs.
buildTypes{
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFile = file("$project.buildDir/apk/test.apk")
}
}
}
Solution for gradle3:
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = applicationId;
outputFileName += "-v" + android.defaultConfig.versionName;
if (variant.buildType.name == "release") {
outputFileName += ".apk";
} else {
outputFileName += "-SNAPSHOT.apk";
}
}
}
defaultConfig {
applicationId "com.dcarl661.awesomelayout"
minSdkVersion 14
targetSdkVersion 25
versionCode 1
versionName "1.0"
//here put it in the section where you have the version stuff
setProperty("archivesBaseName", "AwesomeLayout")
}
defaultConfig {
applicationId "com.example"
def date = new Date();
def formattedDate = date.format('yyyy-MM-dd-HHmmss')
setProperty("archivesBaseName", "Quest_" + versionName + "_"+formattedDate)
}
i did like this. More detailed
If anyone is looking to change apk name outside the Android studio(just to send this file to someone else, as in my case), just right click the app name and change it to whatever you want.
The 'app' part of the name uses the folder name of your application's module (default is 'app'). See the link below on how to change that.
Why is my APK name generic?
The '-debug' prefix is something you can change in the Module settings at Build Types
Maybe this question (and its accepted answer) are interesting for you:
How to set versionName in APK filename using gradle?
It describes how to put your versionName into the APK name.
You have to use the 'Update' part of the accepted answer. Also you need the versionName (in this example) declared in your defaultConfig in the build.gradle file. It does also work if you define versionCode in the build.gradle (as described here: http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Manifest-entries ).

Categories

Resources