|
| 1 | +;;; GNU Guix --- Functional package management for GNU |
| 2 | +;;; Copyright © 2026 Evgenii Lepikhin <johnlepikhin@gmail.com> |
| 3 | +;;; |
| 4 | +;;; This file is not part of GNU Guix. |
| 5 | +;;; |
| 6 | +;;; GNU Guix is free software; you can redistribute it and/or modify it |
| 7 | +;;; under the terms of the GNU General Public License as published by |
| 8 | +;;; the Free Software Foundation; either version 3 of the License, or (at |
| 9 | +;;; your option) any later version. |
| 10 | +;;; |
| 11 | +;;; GNU Guix is distributed in the hope that it will be useful, but |
| 12 | +;;; WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +;;; GNU General Public License for more details. |
| 15 | +;;; |
| 16 | +;;; You should have received a copy of the GNU General Public License |
| 17 | +;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. |
| 18 | + |
| 19 | +(define-module (johnlepikhin packages android-studio) |
| 20 | + #:use-module (guix packages) |
| 21 | + #:use-module (guix download) |
| 22 | + #:use-module (guix gexp) |
| 23 | + #:use-module (guix utils) |
| 24 | + #:use-module (guix build-system copy) |
| 25 | + #:use-module (gnu packages bootstrap) |
| 26 | + #:use-module (gnu packages compression) |
| 27 | + #:use-module (gnu packages crypto) |
| 28 | + #:use-module (gnu packages cups) |
| 29 | + #:use-module (gnu packages elf) |
| 30 | + #:use-module (gnu packages fontutils) |
| 31 | + #:use-module (gnu packages freedesktop) |
| 32 | + #:use-module (gnu packages gcc) |
| 33 | + #:use-module (gnu packages gl) |
| 34 | + #:use-module (gnu packages glib) |
| 35 | + #:use-module (gnu packages gnome) |
| 36 | + #:use-module (gnu packages gtk) |
| 37 | + #:use-module (gnu packages image) |
| 38 | + #:use-module (gnu packages libbsd) |
| 39 | + #:use-module (gnu packages linux) |
| 40 | + #:use-module (gnu packages ncurses) |
| 41 | + #:use-module (gnu packages nss) |
| 42 | + #:use-module (gnu packages tls) |
| 43 | + #:use-module (gnu packages pulseaudio) |
| 44 | + #:use-module (gnu packages vulkan) |
| 45 | + #:use-module (gnu packages xdisorg) |
| 46 | + #:use-module (gnu packages xml) |
| 47 | + #:use-module (gnu packages xorg) |
| 48 | + #:use-module ((gnu packages base) #:select (coreutils glibc)) |
| 49 | + #:use-module ((nonguix licenses) #:prefix license:)) |
| 50 | + |
| 51 | +;; Android Studio, Google's IntelliJ-based IDE for Android. |
| 52 | +;; |
| 53 | +;; Shipped as a ~1.5 GB tarball of prebuilt binaries: the IDE itself, a |
| 54 | +;; bundled JetBrains Runtime (JBR 21 for the Quail line), native helpers |
| 55 | +;; (fsnotifier, restarter, lldb) and a pile of per-plugin shared objects. |
| 56 | +;; Everything carries `/lib64/ld-linux-x86-64.so.2' as its PT_INTERP, so the |
| 57 | +;; install is followed by a blanket patchelf pass. |
| 58 | +;; |
| 59 | +;; Two build-system knobs are mandatory here, for the same reason they are in |
| 60 | +;; (johnlepikhin packages lightburn): this is a self-contained vendor bundle, |
| 61 | +;; so `strip' risks corrupting JBR's binaries and `validate-runpath' would |
| 62 | +;; fail the build on shared objects that resolve their siblings via $ORIGIN. |
| 63 | +;; |
| 64 | +;; Known gap: the Python bundled with the NDK plugin's lldb ships _curses and |
| 65 | +;; _curses_panel extensions built against the ncurses 5 ABI (libncursesw.so.5, |
| 66 | +;; libpanelw.so.5). Guix only has ncurses 6, so those two modules stay |
| 67 | +;; unresolved; they are only reached by lldb's text UI, which nothing in the |
| 68 | +;; agent workflow uses. Every other host binary in the bundle resolves. |
| 69 | +;; |
| 70 | +;; Note on the SDK: Studio downloads and manages its own SDK under |
| 71 | +;; $ANDROID_HOME at run time. Those binaries are *not* patched by this |
| 72 | +;; package -- (johnlepikhin devel android) ships `android-sdk-patch' for |
| 73 | +;; that. Studio also runs its own adb; do not put a second adb on PATH, the |
| 74 | +;; two fight over port 5037. |
| 75 | +(define-public android-studio |
| 76 | + (package |
| 77 | + (name "android-studio") |
| 78 | + (version "2026.1.3.8") ;Quail 3 Patch 1 |
| 79 | + (source |
| 80 | + (origin |
| 81 | + (method url-fetch) |
| 82 | + (uri (string-append "https://edgedl.me.gvt1.com/android/studio/ide-zips/" |
| 83 | + version "/android-studio-quail3-patch1-linux.tar.gz")) |
| 84 | + (file-name (string-append "android-studio-" version ".tar.gz")) |
| 85 | + ;; sha256 published alongside the download on developer.android.com, |
| 86 | + ;; i.e. from a channel independent of the TLS session that fetches the |
| 87 | + ;; tarball itself. |
| 88 | + (sha256 |
| 89 | + (base32 "0py37pqpsxb0x341ay4498pwqn6k1cw42cms5zw16yvldrfyxmav")))) |
| 90 | + (build-system copy-build-system) |
| 91 | + (arguments |
| 92 | + (list |
| 93 | + ;; Prebuilt, self-contained JetBrains bundle: stripping and RUNPATH |
| 94 | + ;; validation make no sense here and only break the vendored runtime. |
| 95 | + #:strip-binaries? #f |
| 96 | + #:validate-runpath? #f |
| 97 | + #:modules '((guix build copy-build-system) |
| 98 | + (guix build utils) |
| 99 | + (ice-9 binary-ports) |
| 100 | + (rnrs bytevectors) |
| 101 | + (srfi srfi-1) |
| 102 | + (srfi srfi-13)) |
| 103 | + #:install-plan |
| 104 | + #~'(("." "lib/android-studio")) |
| 105 | + #:phases |
| 106 | + #~(modify-phases %standard-phases |
| 107 | + (add-after 'install 'patch-elf |
| 108 | + (lambda* (#:key inputs #:allow-other-keys) |
| 109 | + (let* ((app (string-append #$output "/lib/android-studio")) |
| 110 | + (interpreter (string-append |
| 111 | + (assoc-ref inputs "glibc") |
| 112 | + #$(glibc-dynamic-linker))) |
| 113 | + ;; $ORIGIN leads, and it is not decoration: the bundled |
| 114 | + ;; llama.cpp, lldb and layoutlib resolve siblings out of |
| 115 | + ;; their own directory (libggml-base.so.0, |
| 116 | + ;; libpython3.11.so.1.0, ...), and dropping it in favour |
| 117 | + ;; of a purely absolute RUNPATH breaks every one of |
| 118 | + ;; them. jbr/lib/server holds libjvm.so, which the rest |
| 119 | + ;; of jbr/lib links against. |
| 120 | + (rpath (string-join |
| 121 | + (append (list "$ORIGIN" |
| 122 | + ;; bin/ next to lib64/ is how the |
| 123 | + ;; bundled lldb is laid out: |
| 124 | + ;; LLDBFrontend and |
| 125 | + ;; llvm-symbolizer load liblldb |
| 126 | + ;; and libc++ from a sibling |
| 127 | + ;; directory, not their own. |
| 128 | + "$ORIGIN/../lib64" |
| 129 | + "$ORIGIN/../lib" |
| 130 | + (string-append app "/jbr/lib") |
| 131 | + (string-append app "/jbr/lib/server") |
| 132 | + (string-append app "/lib") |
| 133 | + (string-append app "/bin")) |
| 134 | + (map (lambda (label) |
| 135 | + (string-append |
| 136 | + (assoc-ref inputs label) "/lib")) |
| 137 | + '("alsa-lib" "at-spi2-core" "bzip2" |
| 138 | + "cups" "dbus" "e2fsprogs" |
| 139 | + "fontconfig-minimal" |
| 140 | + "freetype" "gcc:lib" "glib" |
| 141 | + "glibc" "gtk+" "libbsd" |
| 142 | + "libdrm" "libice" "libjpeg-turbo" |
| 143 | + "libpng" "libsecret" "libsm" "libx11" |
| 144 | + "libxcb" "libxcomposite" |
| 145 | + "libxcursor" "libxdamage" |
| 146 | + "libxext" "libxfixes" "libxi" |
| 147 | + "libxkbcommon" "libxkbfile" |
| 148 | + "libxcrypt" "libxml2" "libxrandr" |
| 149 | + "libxrender" "libxtst" "mesa" |
| 150 | + "ncurses" "nspr" "nss" "openssl" |
| 151 | + "pulseaudio" "vulkan-loader" |
| 152 | + "wayland" "zlib"))) |
| 153 | + ":"))) |
| 154 | + |
| 155 | + ;; Payloads pushed to the phone or emulator rather than run |
| 156 | + ;; here: the screen-sharing agent, perfetto, the profiler's |
| 157 | + ;; JVMTI agent. Android's linker resolves those against its |
| 158 | + ;; own namespace, so a store RUNPATH is meaningless and |
| 159 | + ;; rewriting them only alters what gets installed on the |
| 160 | + ;; device. They are identified by sitting in a directory |
| 161 | + ;; named after an Android ABI -- excluding the whole of |
| 162 | + ;; plugins/android/resources/ would be wrong, since JNI |
| 163 | + ;; helpers such as native/libjni_object_tagger.so and |
| 164 | + ;; simpleperf/linux-x86_64/ live there too and do run here. |
| 165 | + (define %android-abis |
| 166 | + '("arm64-v8a" "armeabi-v7a" "armeabi" "x86" "x86_64" |
| 167 | + "riscv64" "mips" "mips64")) |
| 168 | + |
| 169 | + (define (device-payload? file) |
| 170 | + (any (lambda (abi) |
| 171 | + (string-contains file (string-append "/" abi "/"))) |
| 172 | + %android-abis)) |
| 173 | + (define (host-elf? file) |
| 174 | + ;; The bundle is mostly jars, XML and PNGs that have no |
| 175 | + ;; business reaching patchelf, and it also carries ARM |
| 176 | + ;; builds of the device payloads. The 20-byte header |
| 177 | + ;; settles both: bytes 0-3 are the magic, bytes 18-19 are |
| 178 | + ;; e_machine (0x3e = EM_X86_64, little endian). Symlinks |
| 179 | + ;; are skipped -- their target is visited on its own. |
| 180 | + (and (eq? 'regular (stat:type (lstat file))) |
| 181 | + (call-with-input-file file |
| 182 | + (lambda (port) |
| 183 | + (let ((header (get-bytevector-n port 20))) |
| 184 | + (and (bytevector? header) |
| 185 | + (= 20 (bytevector-length header)) |
| 186 | + (= #x7f (bytevector-u8-ref header 0)) |
| 187 | + (= #x45 (bytevector-u8-ref header 1)) |
| 188 | + (= #x4c (bytevector-u8-ref header 2)) |
| 189 | + (= #x46 (bytevector-u8-ref header 3)) |
| 190 | + (= #x3e (bytevector-u8-ref header 18)) |
| 191 | + (= #x00 (bytevector-u8-ref header 19))))) |
| 192 | + #:binary #t))) |
| 193 | + |
| 194 | + (for-each |
| 195 | + (lambda (file) |
| 196 | + (if (false-if-exception |
| 197 | + (invoke/quiet "patchelf" "--print-interpreter" file)) |
| 198 | + (invoke "patchelf" "--set-interpreter" interpreter |
| 199 | + "--set-rpath" rpath file) |
| 200 | + ;; No PT_INTERP means one of two things: a shared |
| 201 | + ;; object, which just needs the RUNPATH, or a static |
| 202 | + ;; executable such as bin/fsnotifier, which has no |
| 203 | + ;; .dynamic section for patchelf to rewrite and needs |
| 204 | + ;; nothing from us anyway. Only the former can be |
| 205 | + ;; told apart by trying. |
| 206 | + (unless (false-if-exception |
| 207 | + (invoke/quiet "patchelf" "--set-rpath" |
| 208 | + rpath file)) |
| 209 | + (format #t "patch-elf: leaving static ~a alone~%" |
| 210 | + file)))) |
| 211 | + (filter (lambda (file) |
| 212 | + (and (not (device-payload? file)) |
| 213 | + (host-elf? file))) |
| 214 | + (find-files app ".*" #:stat lstat)))))) |
| 215 | + (add-after 'patch-elf 'install-launcher |
| 216 | + (lambda* (#:key inputs #:allow-other-keys) |
| 217 | + (let* ((app (string-append #$output "/lib/android-studio")) |
| 218 | + (bin (string-append #$output "/bin")) |
| 219 | + (apps (string-append #$output "/share/applications")) |
| 220 | + (icons (string-append #$output |
| 221 | + "/share/icons/hicolor/scalable/apps")) |
| 222 | + ;; Recent releases ship a native `bin/studio'; older |
| 223 | + ;; ones only the `bin/studio.sh' shell launcher. |
| 224 | + (launcher (if (file-exists? (string-append app "/bin/studio")) |
| 225 | + (string-append app "/bin/studio") |
| 226 | + (string-append app "/bin/studio.sh")))) |
| 227 | + (mkdir-p bin) |
| 228 | + (call-with-output-file (string-append bin "/android-studio") |
| 229 | + (lambda (port) |
| 230 | + ;; STUDIO_JDK keeps the IDE on its bundled JetBrains |
| 231 | + ;; Runtime rather than whatever JDK the profile exports |
| 232 | + ;; through JAVA_HOME for Gradle. |
| 233 | + ;; |
| 234 | + ;; gsettings is not optional: IntelliJ shells out to it |
| 235 | + ;; while working out whether it can disable the input |
| 236 | + ;; method, and a missing binary there aborts startup with |
| 237 | + ;; "Start Failed / Cannot run program gsettings". |
| 238 | + ;; |
| 239 | + ;; libsecret (credential store) and libe2p from |
| 240 | + ;; e2fsprogs (FileSystemUtil, for the ext4 case- |
| 241 | + ;; sensitivity flag) are both loaded by JNA under bare |
| 242 | + ;; names, so they are found through the loader's search |
| 243 | + ;; path rather than any RUNPATH we could patch in. Only |
| 244 | + ;; those two directories go into LD_LIBRARY_PATH -- the |
| 245 | + ;; variable is inherited by Gradle, adb and the emulator, |
| 246 | + ;; and a broader value would start overriding the |
| 247 | + ;; libraries they resolve through their own RUNPATH. |
| 248 | + (format port "#!/bin/sh~@ |
| 249 | + export STUDIO_JDK=\"~a/jbr\"~@ |
| 250 | + export PATH=\"~a:~a:$PATH\"~@ |
| 251 | + export LD_LIBRARY_PATH=\"~a:~a${LD_LIBRARY_PATH:+:\ |
| 252 | +$LD_LIBRARY_PATH}\"~@ |
| 253 | + exec \"~a\" \"$@\"~%" |
| 254 | + app |
| 255 | + (string-append (assoc-ref inputs "glib:bin") "/bin") |
| 256 | + (string-append (assoc-ref inputs "coreutils") "/bin") |
| 257 | + (string-append (assoc-ref inputs "libsecret") "/lib") |
| 258 | + (string-append (assoc-ref inputs "e2fsprogs") "/lib") |
| 259 | + launcher))) |
| 260 | + (chmod (string-append bin "/android-studio") #o755) |
| 261 | + |
| 262 | + (mkdir-p icons) |
| 263 | + (let ((svg (string-append app "/bin/studio.svg")) |
| 264 | + (png (string-append app "/bin/studio.png"))) |
| 265 | + (when (file-exists? svg) |
| 266 | + (symlink svg (string-append icons "/android-studio.svg"))) |
| 267 | + (when (file-exists? png) |
| 268 | + (let ((dir (string-append #$output |
| 269 | + "/share/icons/hicolor/256x256/apps"))) |
| 270 | + (mkdir-p dir) |
| 271 | + (symlink png (string-append dir "/android-studio.png"))))) |
| 272 | + |
| 273 | + (mkdir-p apps) |
| 274 | + (call-with-output-file |
| 275 | + (string-append apps "/android-studio.desktop") |
| 276 | + (lambda (port) |
| 277 | + (format port "[Desktop Entry]~@ |
| 278 | + Type=Application~@ |
| 279 | + Name=Android Studio~@ |
| 280 | + Comment=Integrated development environment for Android~@ |
| 281 | + Exec=~a/bin/android-studio %f~@ |
| 282 | + Icon=android-studio~@ |
| 283 | + Terminal=false~@ |
| 284 | + Categories=Development;IDE;~@ |
| 285 | + StartupWMClass=jetbrains-studio~@ |
| 286 | + StartupNotify=true~%" |
| 287 | + #$output))))))))) |
| 288 | + (native-inputs (list patchelf)) |
| 289 | + (inputs |
| 290 | + `(("alsa-lib" ,alsa-lib) |
| 291 | + ("at-spi2-core" ,at-spi2-core) |
| 292 | + ;; bzip2 and libxcrypt are for the Python that ships with the bundled |
| 293 | + ;; lldb; its _bz2 and _crypt extension modules need them. |
| 294 | + ("bzip2" ,bzip2) |
| 295 | + ("coreutils" ,coreutils) |
| 296 | + ("cups" ,cups) |
| 297 | + ("dbus" ,dbus) |
| 298 | + ;; libe2p: FileSystemUtil reads the ext4 case-sensitivity flag. |
| 299 | + ("e2fsprogs" ,e2fsprogs) |
| 300 | + ("fontconfig-minimal" ,fontconfig) |
| 301 | + ("freetype" ,freetype) |
| 302 | + ("gcc:lib" ,gcc "lib") |
| 303 | + ("glib" ,glib) |
| 304 | + ;; gsettings lives in the "bin" output, not the default one. |
| 305 | + ("glib:bin" ,glib "bin") |
| 306 | + ("glibc" ,glibc) |
| 307 | + ("gtk+" ,gtk+) |
| 308 | + ("libbsd" ,libbsd) |
| 309 | + ("libdrm" ,libdrm) |
| 310 | + ("libice" ,libice) |
| 311 | + ("libjpeg-turbo" ,libjpeg-turbo) |
| 312 | + ("libpng" ,libpng) |
| 313 | + ;; The credential store loads libsecret through JNA. |
| 314 | + ("libsecret" ,libsecret) |
| 315 | + ("libsm" ,libsm) |
| 316 | + ("libx11" ,libx11) |
| 317 | + ("libxcb" ,libxcb) |
| 318 | + ("libxcomposite" ,libxcomposite) |
| 319 | + ("libxcursor" ,libxcursor) |
| 320 | + ("libxdamage" ,libxdamage) |
| 321 | + ("libxext" ,libxext) |
| 322 | + ("libxfixes" ,libxfixes) |
| 323 | + ("libxi" ,libxi) |
| 324 | + ("libxkbcommon" ,libxkbcommon) |
| 325 | + ;; The emulator's bundled qemu reaches for libxkbfile, libbsd, nspr |
| 326 | + ;; and a flattened nss on top of the usual X11 set. |
| 327 | + ("libxkbfile" ,libxkbfile) |
| 328 | + ("libxcrypt" ,libxcrypt) |
| 329 | + ("libxml2" ,libxml2) |
| 330 | + ("libxrandr" ,libxrandr) |
| 331 | + ("libxrender" ,libxrender) |
| 332 | + ("libxtst" ,libxtst) |
| 333 | + ("mesa" ,mesa) |
| 334 | + ("ncurses" ,ncurses) |
| 335 | + ("nspr" ,nspr) |
| 336 | + ("nss" ,nss) |
| 337 | + ;; The Gemini plugin bundles llama.cpp, whose server links libssl. |
| 338 | + ("openssl" ,openssl) |
| 339 | + ("pulseaudio" ,pulseaudio) |
| 340 | + ("vulkan-loader" ,vulkan-loader) |
| 341 | + ;; JBR's Wayland-native AWT backend and splash screen link against |
| 342 | + ;; libwayland-client directly. |
| 343 | + ("wayland" ,wayland) |
| 344 | + ("zlib" ,zlib))) |
| 345 | + (supported-systems '("x86_64-linux")) |
| 346 | + (home-page "https://developer.android.com/studio") |
| 347 | + (synopsis "Official integrated development environment for Android") |
| 348 | + (description |
| 349 | + "Android Studio is Google's IntelliJ-based IDE for Android development. |
| 350 | +It provides the Gradle-backed build system, a layout editor with Compose |
| 351 | +preview, profilers, a debugger, the Device Manager for physical and virtual |
| 352 | +devices, and Logcat. |
| 353 | +
|
| 354 | +This package installs the upstream bundle together with its JetBrains Runtime. |
| 355 | +The SDK itself is not included: Studio downloads it at run time into |
| 356 | +@env{ANDROID_HOME}, and those binaries need the @command{android-sdk-patch} |
| 357 | +helper from @code{(johnlepikhin devel android)} to run on Guix System.") |
| 358 | + ;; Governed by the Android Software Development Kit License Agreement. |
| 359 | + (license (license:nonfree "https://developer.android.com/studio/terms")))) |
0 commit comments