Files
nixpkgs/pkgs/development/mobile/androidenv/compose-android-packages.nix
T
Morgan Jones 03d6e4bc46 androidenv: support nonstandard SDK versions
Google have been releasing 'nonstandard' SDK versions for a while. Named
things like CANARY, UpsideDownCake, 36x, and so on, androidenv has been
generally unable to use them, instead preferring Google's officially
supported Android SDKs corresponding to the
[API levels](https://apilevels.com/) (e.g. "35" or "36").

Updates to those SDK versions have generally come in the form of
updates to Google's repositories that repo.json picks up.
These are mostly noneventful for end users, unless a user experienced a
bug in an API definition, then it tended to be picked up with an
androidenv repo update.

API 36.1 changes this. It's not just a number (so it doesn't correspond
to a unique API level). The previous attempt in #470569 simply preferred
the newer version to attempt to emulate the old behavior.

Unfortunately, this now requires a Gradle change (see #472561). So this
means we should just support all the strange SDK versions now and have a
test for it.

Note that only depending on "latest" (the default) will properly pick
these up as the latest, but using a min and max SDK version or
numLatestPlatformVersions > 1 will create a range of SDK versions as
before, which means you may miss (e.g.) 36.1 but will still have 36.
Considering that this requires `compileSdk "android-36.1"`
instead of a simple `compileSdk 36` in Gradle, it's recommended to
explicitly specify platformVersions if you depend on this change,
or use the defaults, where it will only install the latest.
2025-12-22 23:45:00 -08:00

806 lines
24 KiB
Nix

{
callPackage,
stdenv,
stdenvNoCC,
lib,
fetchurl,
ruby,
writeText,
licenseAccepted ? false,
meta,
}:
let
# Coerces a string to an int.
coerceInt = val: if lib.isInt val then val else lib.toIntBase10 val;
coerceIntVersion = v: coerceInt (lib.versions.major (toString v));
# Parses a single version, substituting "latest" with the latest version.
parseVersion =
repo: key: version:
if version == "latest" then repo.latest.${key} else version;
# Parses a list of versions, substituting "latest" with the latest version.
parseVersions =
repo: key: versions:
lib.sort (a: b: lib.strings.compareVersions (toString a) (toString b) > 0) (
lib.unique (map (parseVersion repo key) versions)
);
in
{
repoJson ? ./repo.json,
repoXmls ? null,
repo ? (
# Reads the repo JSON. If repoXmls is provided, will build a repo JSON into the Nix store.
if repoXmls != null then
let
# Uses update.rb to create a repo spec.
mkRepoJson =
{
packages ? [ ],
images ? [ ],
addons ? [ ],
}:
let
mkRepoRuby = (
ruby.withPackages (
pkgs: with pkgs; [
slop
curb
nokogiri
]
)
);
mkRepoRubyArguments = lib.lists.flatten [
(map (package: [
"--packages"
"${package}"
]) packages)
(map (image: [
"--images"
"${image}"
]) images)
(map (addon: [
"--addons"
"${addon}"
]) addons)
];
in
stdenvNoCC.mkDerivation {
name = "androidenv-repo-json";
buildInputs = [ mkRepoRuby ];
preferLocalBuild = true;
unpackPhase = "true";
buildPhase = ''
env ruby -e 'load "${./update.rb}"' -- ${lib.escapeShellArgs mkRepoRubyArguments} --input /dev/null --output repo.json
'';
installPhase = ''
mv repo.json $out
'';
};
repoXmlSpec = {
packages = repoXmls.packages or [ ];
images = repoXmls.images or [ ];
addons = repoXmls.addons or [ ];
};
in
lib.importJSON "${mkRepoJson repoXmlSpec}"
else
lib.importJSON repoJson
),
cmdLineToolsVersion ? "latest",
toolsVersion ? "latest",
platformToolsVersion ? "latest",
buildToolsVersions ? [ "latest" ],
includeEmulator ? false,
emulatorVersion ? "latest",
minPlatformVersion ? null,
maxPlatformVersion ? "latest",
numLatestPlatformVersions ? 1,
platformVersions ?
if minPlatformVersion != null && maxPlatformVersion != null then
# Range between min and max, inclusive.
let
minPlatformVersion' = parseVersion repo "platforms" minPlatformVersion;
maxPlatformVersion' = parseVersion repo "platforms" maxPlatformVersion;
minPlatformVersionInt = coerceIntVersion minPlatformVersion';
maxPlatformVersionInt = coerceIntVersion maxPlatformVersion';
range = lib.range (lib.min minPlatformVersionInt maxPlatformVersionInt) (
lib.max minPlatformVersionInt maxPlatformVersionInt
);
in
# Don't use the actual latest version in lieu of the rounded version here,
# since when Google upgrades it would have the nasty side effect of being
# unstable and picking 35 -> 36 -> 37 instead of 35 -> 36.1 -> 37.
# Best to stay consistent.
#
# However, if only one platform is requested and it's the latest (which is the default),
# we should use it.
if lib.length range == 1 then lib.singleton maxPlatformVersion' else range
else
# Use numLatestPlatformVersions with a lower cutoff of minPlatformVersion (defaulting to 1)
# to determine how many of the latest *major* versions we should pick.
let
minPlatformVersionInt =
if minPlatformVersion == null then
1
else
coerceIntVersion (parseVersion repo "platforms" minPlatformVersion);
latestPlatformVersionInt = lib.max minPlatformVersionInt (coerceIntVersion repo.latest.platforms);
firstPlatformVersionInt = lib.max minPlatformVersionInt (
latestPlatformVersionInt - (lib.max 1 numLatestPlatformVersions) + 1
);
range = lib.range firstPlatformVersionInt latestPlatformVersionInt;
in
# Ditto, see above.
if lib.length range == 1 then lib.singleton repo.latest.platforms else range,
includeSources ? false,
includeSystemImages ? false,
systemImageTypes ? [
"google_apis"
"google_apis_playstore"
],
abiVersions ? [
"x86"
"x86_64"
"armeabi-v7a"
"arm64-v8a"
],
# cmake has precompiles on x86_64 and Darwin platforms. Default to true there for compatibility.
includeCmake ? stdenv.hostPlatform.isx86_64 || stdenv.hostPlatform.isDarwin,
cmakeVersions ? [ "latest" ],
includeNDK ? false,
ndkVersion ? "latest",
ndkVersions ? [ ndkVersion ],
useGoogleAPIs ? false,
useGoogleTVAddOns ? false,
includeExtras ? [ ],
extraLicenses ? [ ],
}:
let
# Resolve all the platform versions.
platformVersions' = parseVersions repo "platforms" platformVersions;
# Determine the Android os identifier from Nix's system identifier
os =
{
x86_64-linux = "linux";
x86_64-darwin = "macosx";
aarch64-linux = "linux";
aarch64-darwin = "macosx";
}
.${stdenv.hostPlatform.system} or "all";
# Determine the Android arch identifier from Nix's system identifier
arch =
{
x86_64-linux = "x64";
x86_64-darwin = "x64";
aarch64-linux = "aarch64";
aarch64-darwin = "aarch64";
}
.${stdenv.hostPlatform.system} or "all";
# Converts all 'archives' keys in a repo spec to fetchurl calls.
fetchArchives =
attrSet:
lib.attrsets.mapAttrsRecursive (
path: value:
if (builtins.elemAt path (builtins.length path - 1)) == "archives" then
let
validArchives = builtins.filter (
archive:
let
isTargetOs =
if builtins.hasAttr "os" archive then archive.os == os || archive.os == "all" else true;
isTargetArch =
if builtins.hasAttr "arch" archive then archive.arch == arch || archive.arch == "all" else true;
in
isTargetOs && isTargetArch
) value;
packageInfo = lib.attrByPath (lib.sublist 0 (builtins.length path - 1) path) null attrSet;
in
lib.optionals (builtins.length validArchives > 0) (
lib.last (
map (
archive:
(fetchurl {
pname = packageInfo.name;
version = packageInfo.revision;
inherit (archive) url sha1;
inherit meta;
passthru = {
info = packageInfo;
};
}).overrideAttrs
(
finalAttrs: previousAttrs: {
# fetchurl prioritize `pname` and `version` over the specified `name`,
# so specify custom `name` in an override.
name = baseNameOf (lib.head (finalAttrs.urls));
}
)
) validArchives
)
)
else
value
) attrSet;
# Converts the repo attrset into fetch calls.
allArchives = {
packages = fetchArchives repo.packages;
system-images = fetchArchives repo.images;
addons = fetchArchives repo.addons;
extras = fetchArchives repo.extras;
};
# Lift the archives to the package level for easy search,
# and add recurseIntoAttrs to all of them.
allPackages =
let
liftedArchives = lib.attrsets.mapAttrsRecursiveCond (value: !(builtins.hasAttr "archives" value)) (
path: value:
if (value.archives or null) != null && (value.archives or [ ]) != [ ] then
lib.dontRecurseIntoAttrs value.archives
else
null
) allArchives;
# Creates a version key from a name.
# Converts things like 'extras;google;auto' to 'extras-google-auto'
toVersionKey =
name:
let
normalizedName = lib.replaceStrings [ ";" "." ] [ "-" "_" ] name;
versionParts = lib.match "^([0-9][0-9\\.]*)(.*)$" normalizedName;
in
if versionParts == null then normalizedName else "v" + lib.concatStrings versionParts;
recurse = lib.mapAttrs' (
name: value:
if builtins.isAttrs value && (value.recurseForDerivations or true) then
lib.nameValuePair (toVersionKey name) (lib.recurseIntoAttrs (recurse value))
else
lib.nameValuePair (toVersionKey name) value
);
in
lib.recurseIntoAttrs (recurse liftedArchives);
# Converts a license name to a list of license texts.
mkLicenses = licenseName: repo.licenses.${licenseName};
# Converts a list of license names to a flattened list of license texts.
# Just used for displaying licenses.
mkLicenseTexts =
licenseNames:
lib.lists.flatten (
map (
licenseName: map (licenseText: "--- ${licenseName} ---\n${licenseText}") (mkLicenses licenseName)
) licenseNames
);
# Converts a license name to a list of license hashes.
mkLicenseHashes =
licenseName: map (licenseText: builtins.hashString "sha1" licenseText) (mkLicenses licenseName);
# The list of all license names we're accepting. Put android-sdk-license there
# by default.
licenseNames = lib.lists.unique (
[
"android-sdk-license"
]
++ extraLicenses
);
# Returns true if the given version exists.
hasVersion =
packages: package: version:
lib.hasAttrByPath [ package (toString version) ] packages;
# Displays a nice error message that includes the available options if a version doesn't exist.
# Note that allPackages can be a list of package sets, or a single package set. Pass a list if
# you want to prioritize elements to the left (e.g. for passing a platform major version).
checkVersion =
allPackages: package: version:
let
# Convert the package sets to a list.
allPackages' = if lib.isList allPackages then allPackages else lib.singleton allPackages;
# Pick the first package set where we have the version.
packageSet = lib.findFirst (packages: hasVersion packages package version) null allPackages';
in
if packageSet == null then
throw ''
The version ${toString version} is missing in package ${package}.
The only available versions are ${
lib.concatStringsSep ", " (
lib.attrNames (lib.foldl (s: x: s // (x.${package} or { })) { } allPackages')
)
}.
''
else
packageSet.${package}.${toString version};
# Returns true if we should link the specified plugins.
shouldLink =
check: packages:
assert builtins.isList packages;
if check == true then
true
else if check == false then
false
else if check == "if-supported" then
let
hasSrc =
package: package.src != null && (builtins.isList package.src -> builtins.length package.src > 0);
in
packages != [ ] && lib.all hasSrc packages
else
throw "Invalid argument ${toString check}; use true, false, or if-supported";
# Function that automatically links all plugins for which multiple versions can coexist
linkPlugins =
{
name,
plugins,
check ? true,
}:
lib.optionalString (shouldLink check plugins) ''
mkdir -p ${name}
${lib.concatMapStrings (plugin: ''
ln -s ${plugin}/libexec/android-sdk/${name}/* ${name}
'') plugins}
'';
# Function that automatically links all NDK plugins.
linkNdkPlugins =
{
name,
plugins,
rootName ? name,
check ? true,
}:
lib.optionalString (shouldLink check plugins) ''
mkdir -p ${rootName}
${lib.concatMapStrings (plugin: ''
ln -s ${plugin}/libexec/android-sdk/${name} ${rootName}/${plugin.version}
'') plugins}
'';
# Function that automatically links the default NDK plugin.
linkNdkPlugin =
{
name,
plugin,
check,
}:
lib.optionalString (shouldLink check [ plugin ]) ''
ln -s ${plugin}/libexec/android-sdk/${name} ${name}
'';
# Function that automatically links a plugin for which only one version exists
linkPlugin =
{
name,
plugin,
check ? true,
}:
lib.optionalString (shouldLink check [ plugin ]) ''
ln -s ${plugin}/libexec/android-sdk/${name} ${name}
'';
linkSystemImages =
{ images, check }:
lib.optionalString (shouldLink check images) ''
mkdir -p system-images
${lib.concatMapStrings (system-image: ''
apiVersion=$(basename $(echo ${system-image}/libexec/android-sdk/system-images/*))
type=$(basename $(echo ${system-image}/libexec/android-sdk/system-images/*/*))
mkdir -p system-images/$apiVersion
ln -s ${system-image}/libexec/android-sdk/system-images/$apiVersion/$type system-images/$apiVersion/$type
'') images}
'';
# Links all plugins related to a requested platform
linkPlatformPlugins =
{
name,
plugins,
check,
}:
lib.optionalString (shouldLink check plugins) ''
mkdir -p ${name}
${lib.concatMapStrings (plugin: ''
ln -s ${plugin}/libexec/android-sdk/${name}/* ${name}
'') plugins}
''; # */
in
lib.recurseIntoAttrs rec {
deployAndroidPackages = callPackage ./deploy-androidpackages.nix {
inherit
stdenv
lib
mkLicenses
meta
os
arch
;
};
deployAndroidPackage = (
{
package,
buildInputs ? [ ],
patchInstructions ? "",
meta ? { },
...
}@args:
let
extraParams = removeAttrs args [
"package"
"os"
"arch"
"buildInputs"
"patchInstructions"
];
in
deployAndroidPackages (
{
inherit buildInputs;
packages = [ package ];
patchesInstructions = {
"${package.name}" = patchInstructions;
};
}
// extraParams
)
);
all = allPackages;
platform-tools = callPackage ./platform-tools.nix {
inherit
deployAndroidPackage
os
arch
meta
;
package = checkVersion allArchives.packages "platform-tools" (
parseVersion repo "platform-tools" platformToolsVersion
);
};
tools = callPackage ./tools.nix {
inherit
deployAndroidPackage
os
arch
meta
;
package = checkVersion allArchives.packages "tools" (parseVersion repo "tools" toolsVersion);
postInstall = ''
${linkPlugin {
name = "platform-tools";
plugin = platform-tools;
}}
${linkPlugin {
name = "emulator";
plugin = emulator;
check = includeEmulator;
}}
'';
};
build-tools = map (
version:
callPackage ./build-tools.nix {
inherit
deployAndroidPackage
os
arch
meta
;
package = checkVersion allArchives.packages "build-tools" version;
postInstall = ''
${linkPlugin {
name = "tools";
plugin = tools;
check = toolsVersion != null;
}}
'';
}
) (parseVersions repo "build-tools" buildToolsVersions);
emulator = callPackage ./emulator.nix {
inherit
deployAndroidPackage
os
arch
meta
;
package = checkVersion allArchives.packages "emulator" (
parseVersion repo "emulator" emulatorVersion
);
postInstall = ''
${linkSystemImages {
images = system-images;
check = includeSystemImages;
}}
'';
};
# This is a list of the chosen API levels, as integers.
platformVersions = platformVersions';
platforms = map (
version:
deployAndroidPackage {
package = checkVersion allArchives.packages "platforms" version;
}
) platformVersions';
sources = map (
version:
deployAndroidPackage {
package = checkVersion allArchives.packages "sources" version;
}
) platformVersions';
system-images = lib.flatten (
map (
apiVersion:
map (
type:
# Deploy all system images with the same systemImageType in one derivation to avoid the `null` problem below
# with avdmanager when trying to create an avd!
#
# ```
# $ yes "" | avdmanager create avd --force --name testAVD --package 'system-images;android-33;google_apis;x86_64'
# Error: Package path is not valid. Valid system image paths are:
# null
# ```
let
availablePackages =
map (abiVersion: allArchives.system-images.${toString apiVersion}.${type}.${abiVersion})
(
builtins.filter (
abiVersion: lib.hasAttrByPath [ (toString apiVersion) type abiVersion ] allArchives.system-images
) abiVersions
);
instructions = lib.listToAttrs (
map (package: {
name = package.name;
value = lib.optionalString (lib.hasPrefix "google_apis" type) ''
# Patch 'google_apis' system images so they're recognized by the sdk.
# Without this, `android list targets` shows 'Tag/ABIs : no ABIs' instead
# of 'Tag/ABIs : google_apis*/*' and the emulator fails with an ABI-related error.
sed -i '/^Addon.Vendor/d' source.properties
'';
}) availablePackages
);
in
lib.optionals (availablePackages != [ ]) (deployAndroidPackages {
packages = availablePackages;
patchesInstructions = instructions;
})
) systemImageTypes
) platformVersions'
);
cmake = map (
version:
callPackage ./cmake.nix {
inherit
deployAndroidPackage
os
arch
meta
;
package = checkVersion allArchives.packages "cmake" version;
}
) (parseVersions repo "cmake" cmakeVersions);
# All NDK bundles.
ndk-bundles =
let
# Creates a NDK bundle.
makeNdkBundle =
package:
callPackage ./ndk-bundle {
inherit
deployAndroidPackage
os
arch
platform-tools
meta
package
;
};
in
lib.flatten (
map (
version:
let
package = makeNdkBundle (
allArchives.packages.ndk-bundle.${version} or allArchives.packages.ndk.${version}
);
in
lib.optional (shouldLink includeNDK [ package ]) package
) (parseVersions repo "ndk" ndkVersions)
);
# The "default" NDK bundle.
ndk-bundle = if ndk-bundles == [ ] then null else lib.head ndk-bundles;
# Makes a Google API bundle from supported versions.
google-apis = map (
version:
deployAndroidPackage {
package = (checkVersion allArchives "addons" version).google_apis;
}
) (lib.filter (hasVersion allArchives "addons") platformVersions');
# Makes a Google TV addons bundle from supported versions.
google-tv-addons = map (
version:
deployAndroidPackage {
package = (checkVersion allArchives "addons" version).google_tv_addon;
}
) (lib.filter (hasVersion allArchives "addons") platformVersions');
cmdline-tools-package = checkVersion allArchives.packages "cmdline-tools" (
parseVersion repo "cmdline-tools" cmdLineToolsVersion
);
# This derivation deploys the tools package and symlinks all the desired
# plugins that we want to use. If the license isn't accepted, prints all the licenses
# requested and throws.
androidsdk = callPackage ./cmdline-tools.nix {
inherit
deployAndroidPackage
os
arch
meta
;
package = cmdline-tools-package;
postInstall =
if !licenseAccepted then
throw ''
${builtins.concatStringsSep "\n\n" (mkLicenseTexts licenseNames)}
You must accept the following licenses:
${lib.concatMapStringsSep "\n" (str: " - ${str}") licenseNames}
a)
by setting nixpkgs config option 'android_sdk.accept_license = true;'.
b)
by an environment variable for a single invocation of the nix tools.
$ export NIXPKGS_ACCEPT_ANDROID_SDK_LICENSE=1
''
else
''
# Symlink all requested plugins
${linkPlugin {
name = "platform-tools";
plugin = platform-tools;
}}
${linkPlugin {
name = "tools";
plugin = tools;
check = toolsVersion != null;
}}
${linkPlugins {
name = "build-tools";
plugins = build-tools;
}}
${linkPlugin {
name = "emulator";
plugin = emulator;
check = includeEmulator;
}}
${linkPlugins {
name = "platforms";
plugins = platforms;
}}
${linkPlatformPlugins {
name = "sources";
plugins = sources;
check = includeSources;
}}
${linkPlugins {
name = "cmake";
plugins = cmake;
check = includeCmake;
}}
${linkNdkPlugins {
name = "ndk-bundle";
rootName = "ndk";
plugins = ndk-bundles;
check = includeNDK;
}}
${linkNdkPlugin {
name = "ndk-bundle";
plugin = ndk-bundle;
check = includeNDK;
}}
${linkSystemImages {
images = system-images;
check = includeSystemImages;
}}
${linkPlatformPlugins {
name = "add-ons";
plugins = google-apis;
check = useGoogleAPIs;
}}
${linkPlatformPlugins {
name = "add-ons";
plugins = google-tv-addons;
check = useGoogleTVAddOns;
}}
# Link extras
${lib.concatMapStrings (
identifier:
let
package = allArchives.extras.${identifier};
path = package.path;
extras = callPackage ./extras.nix {
inherit
deployAndroidPackage
package
os
arch
meta
;
};
in
''
targetDir=$(dirname ${path})
mkdir -p $targetDir
ln -s ${extras}/libexec/android-sdk/${path} $targetDir
''
) includeExtras}
# Expose common executables in bin/
mkdir -p $out/bin
for i in ${platform-tools}/bin/*; do
ln -s $i $out/bin
done
${lib.optionalString (shouldLink includeEmulator [ emulator ]) ''
for i in ${emulator}/bin/*; do
ln -s $i $out/bin
done
''}
find $ANDROID_SDK_ROOT/${cmdline-tools-package.path}/bin -type f -executable | while read i; do
ln -s $i $out/bin
done
# Write licenses
mkdir -p licenses
${lib.concatMapStrings (
licenseName:
let
licenseHashes = builtins.concatStringsSep "\n" (mkLicenseHashes licenseName);
licenseHashFile = writeText "androidenv-${licenseName}" licenseHashes;
in
''
ln -s ${licenseHashFile} licenses/${licenseName}
''
) licenseNames}
'';
};
}