Skip to content

Commit f45aeb6

Browse files
committed
feat(android-studio): Add Android Studio Quail 3 Patch 1
A 1.45 GiB bundle of prebuilt binaries -- the IDE, a bundled JetBrains Runtime (OpenJDK 25), native helpers and per-plugin shared objects -- all carrying /lib64/ld-linux-x86-64.so.2, so the install is followed by a blanket patchelf pass. The sha256 is the one published on developer.android.com, i.e. from a channel independent of the TLS session that fetches the tarball. #:strip-binaries? #f and #:validate-runpath? #f are both required, for the reason already recorded in lightburn: this is a vendored bundle, and RUNPATH validation would fail the build at the end of an hour-long one. Five things only surfaced by running it: - bin/fsnotifier is a static ELF with no .dynamic section; patchelf exits non-zero on it and the phase has to tolerate that. - $ORIGIN must lead the RUNPATH. The vendored llama.cpp, lldb and layoutlib resolve siblings out of their own directory, and lldb keeps bin/ and lib64/ apart, hence $ORIGIN/../lib64 as well. - jbr/lib/server holds libjvm.so, which the rest of jbr/lib links. - plugins/android/resources/ mixes host JNI helpers with payloads pushed to the device, so the exclusion keys on Android ABI directory names rather than the whole tree. - gsettings is not optional: IntelliJ shells out to it while deciding whether it can disable the input method, and its absence aborts startup with "Start Failed". libsecret and libe2p are loaded by JNA under bare names, so no RUNPATH reaches them and they go into a deliberately narrow LD_LIBRARY_PATH instead. Of 145 host ELFs two are left unresolved: the _curses and _curses_panel extensions of the Python bundled with the NDK plugin's lldb are built against the ncurses 5 ABI, and Guix has only 6. They are reached solely by lldb's text UI.
1 parent 7d5cef1 commit f45aeb6

1 file changed

Lines changed: 359 additions & 0 deletions

File tree

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
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

Comments
 (0)