1{
2 callPackage,
3 stdenv,
4 stdenvNoCC,
5 lib,
6 fetchurl,
7 ruby,
8 writeText,
9 licenseAccepted ? false,
10 meta,
11}:
12
13let
14 # Coerces a string to an int.
15 coerceInt = val: if lib.isInt val then val else lib.toIntBase10 val;
16
17 # Parses a single version, substituting "latest" with the latest version.
18 parseVersion =
19 repo: key: version:
20 if version == "latest" then repo.latest.${key} else version;
21
22 # Parses a list of versions, substituting "latest" with the latest version.
23 parseVersions =
24 repo: key: versions:
25 lib.unique (map (parseVersion repo key) versions);
26in
27{
28 repoJson ? ./repo.json,
29 repoXmls ? null,
30 repo ? (
31 # Reads the repo JSON. If repoXmls is provided, will build a repo JSON into the Nix store.
32 if repoXmls != null then
33 let
34 # Uses update.rb to create a repo spec.
35 mkRepoJson =
36 {
37 packages ? [ ],
38 images ? [ ],
39 addons ? [ ],
40 }:
41 let
42 mkRepoRuby = (
43 ruby.withPackages (
44 pkgs: with pkgs; [
45 slop
46 curb
47 nokogiri
48 ]
49 )
50 );
51 mkRepoRubyArguments = lib.lists.flatten [
52 (map (package: [
53 "--packages"
54 "${package}"
55 ]) packages)
56 (map (image: [
57 "--images"
58 "${image}"
59 ]) images)
60 (map (addon: [
61 "--addons"
62 "${addon}"
63 ]) addons)
64 ];
65 in
66 stdenvNoCC.mkDerivation {
67 name = "androidenv-repo-json";
68 buildInputs = [ mkRepoRuby ];
69 preferLocalBuild = true;
70 unpackPhase = "true";
71 buildPhase = ''
72 env ruby -e 'load "${./update.rb}"' -- ${lib.escapeShellArgs mkRepoRubyArguments} --input /dev/null --output repo.json
73 '';
74 installPhase = ''
75 mv repo.json $out
76 '';
77 };
78 repoXmlSpec = {
79 packages = repoXmls.packages or [ ];
80 images = repoXmls.images or [ ];
81 addons = repoXmls.addons or [ ];
82 };
83 in
84 lib.importJSON "${mkRepoJson repoXmlSpec}"
85 else
86 lib.importJSON repoJson
87 ),
88 cmdLineToolsVersion ? "latest",
89 toolsVersion ? "latest",
90 platformToolsVersion ? "latest",
91 buildToolsVersions ? [ "latest" ],
92 includeEmulator ? false,
93 emulatorVersion ? "latest",
94 minPlatformVersion ? null,
95 maxPlatformVersion ? "latest",
96 numLatestPlatformVersions ? 1,
97 platformVersions ?
98 if minPlatformVersion != null && maxPlatformVersion != null then
99 let
100 minPlatformVersionInt = coerceInt (parseVersion repo "platforms" minPlatformVersion);
101 maxPlatformVersionInt = coerceInt (parseVersion repo "platforms" maxPlatformVersion);
102 in
103 lib.range (lib.min minPlatformVersionInt maxPlatformVersionInt) (
104 lib.max minPlatformVersionInt maxPlatformVersionInt
105 )
106 else
107 let
108 minPlatformVersionInt =
109 if minPlatformVersion == null then
110 1
111 else
112 coerceInt (parseVersion repo "platforms" minPlatformVersion);
113 latestPlatformVersionInt = lib.max minPlatformVersionInt (coerceInt repo.latest.platforms);
114 firstPlatformVersionInt = lib.max minPlatformVersionInt (
115 latestPlatformVersionInt - (lib.max 1 numLatestPlatformVersions) + 1
116 );
117 in
118 lib.range firstPlatformVersionInt latestPlatformVersionInt,
119 includeSources ? false,
120 includeSystemImages ? false,
121 systemImageTypes ? [
122 "google_apis"
123 "google_apis_playstore"
124 ],
125 abiVersions ? [
126 "x86"
127 "x86_64"
128 "armeabi-v7a"
129 "arm64-v8a"
130 ],
131 # cmake has precompiles on x86_64 and Darwin platforms. Default to true there for compatibility.
132 includeCmake ? stdenv.hostPlatform.isx86_64 || stdenv.hostPlatform.isDarwin,
133 cmakeVersions ? [ "latest" ],
134 includeNDK ? false,
135 ndkVersion ? "latest",
136 ndkVersions ? [ ndkVersion ],
137 useGoogleAPIs ? false,
138 useGoogleTVAddOns ? false,
139 includeExtras ? [ ],
140 extraLicenses ? [ ],
141}:
142
143let
144 # Resolve all the platform versions.
145 platformVersions' = map coerceInt (parseVersions repo "platforms" platformVersions);
146
147 # Determine the Android os identifier from Nix's system identifier
148 os =
149 {
150 x86_64-linux = "linux";
151 x86_64-darwin = "macosx";
152 aarch64-linux = "linux";
153 aarch64-darwin = "macosx";
154 }
155 .${stdenv.hostPlatform.system} or "all";
156
157 # Determine the Android arch identifier from Nix's system identifier
158 arch =
159 {
160 x86_64-linux = "x64";
161 x86_64-darwin = "x64";
162 aarch64-linux = "aarch64";
163 aarch64-darwin = "aarch64";
164 }
165 .${stdenv.hostPlatform.system} or "all";
166
167 # Converts all 'archives' keys in a repo spec to fetchurl calls.
168 fetchArchives =
169 attrSet:
170 lib.attrsets.mapAttrsRecursive (
171 path: value:
172 if (builtins.elemAt path (builtins.length path - 1)) == "archives" then
173 let
174 validArchives = builtins.filter (
175 archive:
176 let
177 isTargetOs =
178 if builtins.hasAttr "os" archive then archive.os == os || archive.os == "all" else true;
179 isTargetArch =
180 if builtins.hasAttr "arch" archive then archive.arch == arch || archive.arch == "all" else true;
181 in
182 isTargetOs && isTargetArch
183 ) value;
184 packageInfo = lib.attrByPath (lib.sublist 0 (builtins.length path - 1) path) null attrSet;
185 in
186 lib.optionals (builtins.length validArchives > 0) (
187 lib.last (
188 map (
189 archive:
190 (fetchurl {
191 inherit (archive) url sha1;
192 inherit meta;
193 passthru = {
194 info = packageInfo;
195 };
196 }).overrideAttrs
197 (prev: {
198 # fetchurl won't generate the correct filename if we specify pname and version,
199 # and we still want the version attribute to show up in search, so specify these in an override
200 pname = packageInfo.name;
201 version = packageInfo.revision;
202 })
203 ) validArchives
204 )
205 )
206 else
207 value
208 ) attrSet;
209
210 # Converts the repo attrset into fetch calls.
211 allArchives = {
212 packages = fetchArchives repo.packages;
213 system-images = fetchArchives repo.images;
214 addons = fetchArchives repo.addons;
215 extras = fetchArchives repo.extras;
216 };
217
218 # Lift the archives to the package level for easy search,
219 # and add recurseIntoAttrs to all of them.
220 allPackages =
221 let
222 liftedArchives = lib.attrsets.mapAttrsRecursiveCond (value: !(builtins.hasAttr "archives" value)) (
223 path: value:
224 if (value.archives or null) != null && (value.archives or [ ]) != [ ] then
225 lib.dontRecurseIntoAttrs value.archives
226 else
227 null
228 ) allArchives;
229
230 # Creates a version key from a name.
231 # Converts things like 'extras;google;auto' to 'extras-google-auto'
232 toVersionKey =
233 name:
234 let
235 normalizedName = lib.replaceStrings [ ";" "." ] [ "-" "_" ] name;
236 versionParts = lib.match "^([0-9][0-9\\.]*)(.*)$" normalizedName;
237 in
238 if versionParts == null then normalizedName else "v" + lib.concatStrings versionParts;
239
240 recurse = lib.mapAttrs' (
241 name: value:
242 if builtins.isAttrs value && (value.recurseForDerivations or true) then
243 lib.nameValuePair (toVersionKey name) (lib.recurseIntoAttrs (recurse value))
244 else
245 lib.nameValuePair (toVersionKey name) value
246 );
247 in
248 lib.recurseIntoAttrs (recurse liftedArchives);
249
250 # Converts a license name to a list of license texts.
251 mkLicenses = licenseName: repo.licenses.${licenseName};
252
253 # Converts a list of license names to a flattened list of license texts.
254 # Just used for displaying licenses.
255 mkLicenseTexts =
256 licenseNames:
257 lib.lists.flatten (
258 builtins.map (
259 licenseName:
260 builtins.map (licenseText: "--- ${licenseName} ---\n${licenseText}") (mkLicenses licenseName)
261 ) licenseNames
262 );
263
264 # Converts a license name to a list of license hashes.
265 mkLicenseHashes =
266 licenseName:
267 builtins.map (licenseText: builtins.hashString "sha1" licenseText) (mkLicenses licenseName);
268
269 # The list of all license names we're accepting. Put android-sdk-license there
270 # by default.
271 licenseNames = lib.lists.unique (
272 [
273 "android-sdk-license"
274 ]
275 ++ extraLicenses
276 );
277
278 # Returns true if the given version exists.
279 hasVersion =
280 packages: package: version:
281 lib.hasAttrByPath [ package (toString version) ] packages;
282
283 # Displays a nice error message that includes the available options if a version doesn't exist.
284 checkVersion =
285 packages: package: version:
286 if hasVersion packages package version then
287 packages.${package}.${toString version}
288 else
289 throw ''
290 The version ${toString version} is missing in package ${package}.
291 The only available versions are ${
292 builtins.concatStringsSep ", " (builtins.attrNames packages.${package})
293 }.
294 '';
295
296 # Returns true if we should link the specified plugins.
297 shouldLink =
298 check: packages:
299 assert builtins.isList packages;
300 if check == true then
301 true
302 else if check == false then
303 false
304 else if check == "if-supported" then
305 let
306 hasSrc =
307 package: package.src != null && (builtins.isList package.src -> builtins.length package.src > 0);
308 in
309 packages != [ ] && lib.all hasSrc packages
310 else
311 throw "Invalid argument ${toString check}; use true, false, or if-supported";
312
313 # Function that automatically links all plugins for which multiple versions can coexist
314 linkPlugins =
315 {
316 name,
317 plugins,
318 check ? true,
319 }:
320 lib.optionalString (shouldLink check plugins) ''
321 mkdir -p ${name}
322 ${lib.concatMapStrings (plugin: ''
323 ln -s ${plugin}/libexec/android-sdk/${name}/* ${name}
324 '') plugins}
325 '';
326
327 # Function that automatically links all NDK plugins.
328 linkNdkPlugins =
329 {
330 name,
331 plugins,
332 rootName ? name,
333 check ? true,
334 }:
335 lib.optionalString (shouldLink check plugins) ''
336 mkdir -p ${rootName}
337 ${lib.concatMapStrings (plugin: ''
338 ln -s ${plugin}/libexec/android-sdk/${name} ${rootName}/${plugin.version}
339 '') plugins}
340 '';
341
342 # Function that automatically links the default NDK plugin.
343 linkNdkPlugin =
344 {
345 name,
346 plugin,
347 check,
348 }:
349 lib.optionalString (shouldLink check [ plugin ]) ''
350 ln -s ${plugin}/libexec/android-sdk/${name} ${name}
351 '';
352
353 # Function that automatically links a plugin for which only one version exists
354 linkPlugin =
355 {
356 name,
357 plugin,
358 check ? true,
359 }:
360 lib.optionalString (shouldLink check [ plugin ]) ''
361 ln -s ${plugin}/libexec/android-sdk/${name} ${name}
362 '';
363
364 linkSystemImages =
365 { images, check }:
366 lib.optionalString (shouldLink check images) ''
367 mkdir -p system-images
368 ${lib.concatMapStrings (system-image: ''
369 apiVersion=$(basename $(echo ${system-image}/libexec/android-sdk/system-images/*))
370 type=$(basename $(echo ${system-image}/libexec/android-sdk/system-images/*/*))
371 mkdir -p system-images/$apiVersion
372 ln -s ${system-image}/libexec/android-sdk/system-images/$apiVersion/$type system-images/$apiVersion/$type
373 '') images}
374 '';
375
376 # Links all plugins related to a requested platform
377 linkPlatformPlugins =
378 {
379 name,
380 plugins,
381 check,
382 }:
383 lib.optionalString (shouldLink check plugins) ''
384 mkdir -p ${name}
385 ${lib.concatMapStrings (plugin: ''
386 ln -s ${plugin}/libexec/android-sdk/${name}/* ${name}
387 '') plugins}
388 ''; # */
389
390in
391lib.recurseIntoAttrs rec {
392 deployAndroidPackages = callPackage ./deploy-androidpackages.nix {
393 inherit
394 stdenv
395 lib
396 mkLicenses
397 meta
398 os
399 arch
400 ;
401 };
402
403 deployAndroidPackage = (
404 {
405 package,
406 buildInputs ? [ ],
407 patchInstructions ? "",
408 meta ? { },
409 ...
410 }@args:
411 let
412 extraParams = removeAttrs args [
413 "package"
414 "os"
415 "arch"
416 "buildInputs"
417 "patchInstructions"
418 ];
419 in
420 deployAndroidPackages (
421 {
422 inherit buildInputs;
423 packages = [ package ];
424 patchesInstructions = {
425 "${package.name}" = patchInstructions;
426 };
427 }
428 // extraParams
429 )
430 );
431
432 all = allPackages;
433
434 platform-tools = callPackage ./platform-tools.nix {
435 inherit
436 deployAndroidPackage
437 os
438 arch
439 meta
440 ;
441 package = checkVersion allArchives.packages "platform-tools" (
442 parseVersion repo "platform-tools" platformToolsVersion
443 );
444 };
445
446 tools = callPackage ./tools.nix {
447 inherit
448 deployAndroidPackage
449 os
450 arch
451 meta
452 ;
453 package = checkVersion allArchives.packages "tools" (parseVersion repo "tools" toolsVersion);
454
455 postInstall = ''
456 ${linkPlugin {
457 name = "platform-tools";
458 plugin = platform-tools;
459 }}
460 ${linkPlugin {
461 name = "emulator";
462 plugin = emulator;
463 check = includeEmulator;
464 }}
465 '';
466 };
467
468 build-tools = map (
469 version:
470 callPackage ./build-tools.nix {
471 inherit
472 deployAndroidPackage
473 os
474 arch
475 meta
476 ;
477 package = checkVersion allArchives.packages "build-tools" version;
478
479 postInstall = ''
480 ${linkPlugin {
481 name = "tools";
482 plugin = tools;
483 check = toolsVersion != null;
484 }}
485 '';
486 }
487 ) (parseVersions repo "build-tools" buildToolsVersions);
488
489 emulator = callPackage ./emulator.nix {
490 inherit
491 deployAndroidPackage
492 os
493 arch
494 meta
495 ;
496 package = checkVersion allArchives.packages "emulator" (
497 parseVersion repo "emulator" emulatorVersion
498 );
499
500 postInstall = ''
501 ${linkSystemImages {
502 images = system-images;
503 check = includeSystemImages;
504 }}
505 '';
506 };
507
508 platformVersions = platformVersions';
509
510 platforms = map (
511 version:
512 deployAndroidPackage {
513 package = checkVersion allArchives.packages "platforms" version;
514 }
515 ) platformVersions';
516
517 sources = map (
518 version:
519 deployAndroidPackage {
520 package = checkVersion allArchives.packages "sources" version;
521 }
522 ) platformVersions';
523
524 system-images = lib.flatten (
525 map (
526 apiVersion:
527 map (
528 type:
529 # Deploy all system images with the same systemImageType in one derivation to avoid the `null` problem below
530 # with avdmanager when trying to create an avd!
531 #
532 # ```
533 # $ yes "" | avdmanager create avd --force --name testAVD --package 'system-images;android-33;google_apis;x86_64'
534 # Error: Package path is not valid. Valid system image paths are:
535 # null
536 # ```
537 let
538 availablePackages =
539 map (abiVersion: allArchives.system-images.${toString apiVersion}.${type}.${abiVersion})
540 (
541 builtins.filter (
542 abiVersion: lib.hasAttrByPath [ (toString apiVersion) type abiVersion ] allArchives.system-images
543 ) abiVersions
544 );
545
546 instructions = builtins.listToAttrs (
547 map (package: {
548 name = package.name;
549 value = lib.optionalString (lib.hasPrefix "google_apis" type) ''
550 # Patch 'google_apis' system images so they're recognized by the sdk.
551 # Without this, `android list targets` shows 'Tag/ABIs : no ABIs' instead
552 # of 'Tag/ABIs : google_apis*/*' and the emulator fails with an ABI-related error.
553 sed -i '/^Addon.Vendor/d' source.properties
554 '';
555 }) availablePackages
556 );
557 in
558 lib.optionals (availablePackages != [ ]) (deployAndroidPackages {
559 packages = availablePackages;
560 patchesInstructions = instructions;
561 })
562 ) systemImageTypes
563 ) platformVersions'
564 );
565
566 cmake = map (
567 version:
568 callPackage ./cmake.nix {
569 inherit
570 deployAndroidPackage
571 os
572 arch
573 meta
574 ;
575 package = checkVersion allArchives.packages "cmake" version;
576 }
577 ) (parseVersions repo "cmake" cmakeVersions);
578
579 # All NDK bundles.
580 ndk-bundles =
581 let
582 # Creates a NDK bundle.
583 makeNdkBundle =
584 package:
585 callPackage ./ndk-bundle {
586 inherit
587 deployAndroidPackage
588 os
589 arch
590 platform-tools
591 meta
592 package
593 ;
594 };
595 in
596 lib.flatten (
597 map (
598 version:
599 let
600 package = makeNdkBundle (
601 allArchives.packages.ndk-bundle.${version} or allArchives.packages.ndk.${version}
602 );
603 in
604 lib.optional (shouldLink includeNDK [ package ]) package
605 ) (parseVersions repo "ndk" ndkVersions)
606 );
607
608 # The "default" NDK bundle.
609 ndk-bundle = if ndk-bundles == [ ] then null else lib.head ndk-bundles;
610
611 # Makes a Google API bundle from supported versions.
612 google-apis = map (
613 version:
614 deployAndroidPackage {
615 package = (checkVersion allArchives "addons" version).google_apis;
616 }
617 ) (lib.filter (hasVersion allArchives "addons") platformVersions');
618
619 # Makes a Google TV addons bundle from supported versions.
620 google-tv-addons = map (
621 version:
622 deployAndroidPackage {
623 package = (checkVersion allArchives "addons" version).google_tv_addon;
624 }
625 ) (lib.filter (hasVersion allArchives "addons") platformVersions');
626
627 cmdline-tools-package = checkVersion allArchives.packages "cmdline-tools" (
628 parseVersion repo "cmdline-tools" cmdLineToolsVersion
629 );
630
631 # This derivation deploys the tools package and symlinks all the desired
632 # plugins that we want to use. If the license isn't accepted, prints all the licenses
633 # requested and throws.
634 androidsdk = callPackage ./cmdline-tools.nix {
635 inherit
636 deployAndroidPackage
637 os
638 arch
639 meta
640 ;
641
642 package = cmdline-tools-package;
643
644 postInstall =
645 if !licenseAccepted then
646 throw ''
647 ${builtins.concatStringsSep "\n\n" (mkLicenseTexts licenseNames)}
648
649 You must accept the following licenses:
650 ${lib.concatMapStringsSep "\n" (str: " - ${str}") licenseNames}
651
652 a)
653 by setting nixpkgs config option 'android_sdk.accept_license = true;'.
654 b)
655 by an environment variable for a single invocation of the nix tools.
656 $ export NIXPKGS_ACCEPT_ANDROID_SDK_LICENSE=1
657 ''
658 else
659 ''
660 # Symlink all requested plugins
661 ${linkPlugin {
662 name = "platform-tools";
663 plugin = platform-tools;
664 }}
665 ${linkPlugin {
666 name = "tools";
667 plugin = tools;
668 check = toolsVersion != null;
669 }}
670 ${linkPlugins {
671 name = "build-tools";
672 plugins = build-tools;
673 }}
674 ${linkPlugin {
675 name = "emulator";
676 plugin = emulator;
677 check = includeEmulator;
678 }}
679 ${linkPlugins {
680 name = "platforms";
681 plugins = platforms;
682 }}
683 ${linkPlatformPlugins {
684 name = "sources";
685 plugins = sources;
686 check = includeSources;
687 }}
688 ${linkPlugins {
689 name = "cmake";
690 plugins = cmake;
691 check = includeCmake;
692 }}
693 ${linkNdkPlugins {
694 name = "ndk-bundle";
695 rootName = "ndk";
696 plugins = ndk-bundles;
697 check = includeNDK;
698 }}
699 ${linkNdkPlugin {
700 name = "ndk-bundle";
701 plugin = ndk-bundle;
702 check = includeNDK;
703 }}
704 ${linkSystemImages {
705 images = system-images;
706 check = includeSystemImages;
707 }}
708 ${linkPlatformPlugins {
709 name = "add-ons";
710 plugins = google-apis;
711 check = useGoogleAPIs;
712 }}
713 ${linkPlatformPlugins {
714 name = "add-ons";
715 plugins = google-tv-addons;
716 check = useGoogleTVAddOns;
717 }}
718
719 # Link extras
720 ${lib.concatMapStrings (
721 identifier:
722 let
723 package = allArchives.extras.${identifier};
724 path = package.path;
725 extras = callPackage ./extras.nix {
726 inherit
727 deployAndroidPackage
728 package
729 os
730 arch
731 meta
732 ;
733 };
734 in
735 ''
736 targetDir=$(dirname ${path})
737 mkdir -p $targetDir
738 ln -s ${extras}/libexec/android-sdk/${path} $targetDir
739 ''
740 ) includeExtras}
741
742 # Expose common executables in bin/
743 mkdir -p $out/bin
744
745 for i in ${platform-tools}/bin/*; do
746 ln -s $i $out/bin
747 done
748
749 ${lib.optionalString (shouldLink includeEmulator [ emulator ]) ''
750 for i in ${emulator}/bin/*; do
751 ln -s $i $out/bin
752 done
753 ''}
754
755 find $ANDROID_SDK_ROOT/${cmdline-tools-package.path}/bin -type f -executable | while read i; do
756 ln -s $i $out/bin
757 done
758
759 # Write licenses
760 mkdir -p licenses
761 ${lib.concatMapStrings (
762 licenseName:
763 let
764 licenseHashes = builtins.concatStringsSep "\n" (mkLicenseHashes licenseName);
765 licenseHashFile = writeText "androidenv-${licenseName}" licenseHashes;
766 in
767 ''
768 ln -s ${licenseHashFile} licenses/${licenseName}
769 ''
770 ) licenseNames}
771 '';
772 };
773}