מבוא
F-Droid פועל להפצת בניות שניתן לשחזר במערכת האקולוגית של תוכנה חופשית ל־Android. המטרה היא לאפשר תהליכי בנייה של תוכנה שכל אחד יכול להריץ שוב ושוב ולשחזר בדיוק את אותו APK כמו המהדורה המקורית. העבודה שלנו מתמקדת בשלושה תחומים עיקריים:
- סביבת הבנייה שלנו מתוכננת כך שיהיה קל לשחזר בניות, ובה בעת היא עצמה ניתנת לשחזור ולביקורת.
- אנו עוקבים אחר תקלות בכלי הבנייה עצמם שמונעות בניות שניתן לשחזר, עוזרים למתחזקי כלי הבנייה לתקן את התקלות האלה, ומתעדים פתרונות עוקפים עבור מפתחי יישומונים כאן, בדף זה.
- אנו עוזרים למפתחי המקור של כל יישומון שמופץ ב־f-droid.org לתקן בעיות בבניות שניתן לשחזר, באמצעות תמיכה למפתחים, פתיחת תקלות והצעת שינויים לקוד המקור.
F-Droid מאמת בניות שניתן לשחזר באמצעות העתקת חתימות של APK בין הבנייה של מפתחי המקור לבין הבנייה מחדש שלנו. כדי לברר אם שרת הבנייה שלנו מצליח לבנות מחדש יישומון באופן שניתן לשחזר, יש לבדוק את „Reproducibility Status” (מצב השחזוריות) בדף של כל יישומון באתר זה. הדבר יכול לעזור לנו לזהות גורמים שמשתנים לאורך זמן.
תקן הזהב בבניות שניתן לשחזר להתמודדות עם בעיות Trusting
Trust
הוא Diverse Double-Compiling (הידור
כפול מגוון). הרעיון המרכזי הוא להשתמש בשתי ערכות נפרדות לחלוטין של כלי בנייה
כדי ליצור בדיוק את אותו קובץ בינרי. קשה לעמוד בתקן זה, אף שהוא בעל ערך
רב. חלק מהעבודה לקראתו אפשר לעשות בהדרגה. לשם כך, F-Droid יכול לשחזר את
קובצי ה־APK שמפתחי המקור בנו בסביבה שלהם. לעיתים קרובות הם נבנים עם שרשראות
כלים שונות או במערכות הפעלה שונות. כדי לראות אילו יישומונים הפעילו גישה זו,
נא לבדוק ב־נתוני העל של
הבנייה
שלהם אם קיימים שדות נתוני העל
Binaries:
או
binary:.
חתימות שניתן לשחזר
F-Droid מאמת בניות שניתן לשחזר באמצעות חתימת ה־APK (סוג של חתימה מוטמעת), מה שמחייב להעתיק את החתימה מ־APK חתום ל־APK לא חתום ואז לבדוק אם האחרון עובר אימות. חתימות v1 (JAR) הישנות מכסות רק את התוכן של ה־APK (למשל, נתוני העל וסדר הרשומות של ה־ZIP אינם רלוונטיים), אבל חתימות v2/v3 מכסות את כל שאר הבתים ב־APK. לכן, קובצי ה־APK חייבים להיות זהים לחלוטין לפני החתימה ואחריה (מלבד החתימה עצמה) כדי שהאימות יצליח.
העתקת החתימה משתמשת באותו אלגוריתם ש־apksigner משתמש בו בעת חתימה על
APK. לכן חשוב שמפתחים (מפתחי המקור) יעשו אותו דבר בעת חתימה על קובצי APK,
ובאופן אידיאלי ישתמשו ב־apksigner ליצירת החתימות. גם apksigner עצמו נבנה
באופן שניתן לשחזר
ב־Debian.
בניות אימות
אנשים או ארגונים רבים יתעניינו בשחזור בניות כדי לוודא שהבניות של f-droid.org תואמות את המקור המקורי ושדבר לא שונה. במקרה כזה, קובצי ה־APK שמתקבלים אינם מתפרסמים להתקנה. שרת האימות מבצע תהליך זה אוטומטית.
המצב
לא מעט בניות כבר עוברות אימות ללא מאמץ נוסף, מאחר שקוד Java מהודר לעיתים
קרובות לאותו bytecode במגוון רחב של גרסאות Java. ה־build-tools של ה־SDK של
Android ייצרו הבדלים בקובצי ה־XML, PNG וכו’ שמתקבלים, אבל בדרך כלל זו אינה
בעיה, מאחר שה־build.gradle כולל את הגרסה המדויקת של build-tools שיש
להשתמש בה.
כל מה שנבנה באמצעות ה־NDK יהיה רגיש הרבה יותר. לדוגמה, גם בבניות שמשתמשות
בדיוק באותה גרסה של ה־NDK (למשל r13b) אבל בפלטפורמות שונות (למשל macOS
לעומת Ubuntu), יהיו הבדלים בקבצים הבינריים שמתקבלים.
בנוסף, נצטרך לשים לב לכל דבר שכולל חותמות זמן או נתיבי בנייה, שרגיש לסדר המיון וכו’.
גם Google פועלת לקראת בניות שניתן לשחזר של יישומוני Android, כך ששימוש בגרסאות עדכניות של ה־SDK של Android עוזר. מקרה ספציפי אחד: החל מ־Gradle Android Plugin v2.2.2, חותמות הזמן בנתוני העל של ה־ZIP של קובץ ה־APK מאופסות אוטומטית.
פרסום קובצי APK עם החתימה של מפתחי המקור
אפשר להגדיר יישומון כך שיפרסם את הקבצים הבינריים החתומים של מפתחי המקור לאחר
אימות שהם תואמים לאלה שנבנו באמצעות מתכון בנייה של fdroiddata. הפרסום
מתבצע רק אם יש התאמה מלאה. המשמעות היא ש־F-Droid יכול לוודא שיישומון הוא
תוכנה חופשית ועדיין להשתמש בחתימות ה־APK של המפתח המקורי. תהליך זה ממומש
כחלק מ־fdroid
publish.
בדיקת השחזוריות בשלב הפרסום פועלת לפי ההיגיון הבא:
פרסום בלעדי של קובצי APK חתומים על ידי המפתחים (מפתחי המקור)
בגישה זו, כל מה שבנתוני העל צריך להיות כרגיל, בתוספת ההנחיה
Binaries או
Builds.binary כדי לציין מהיכן
להוריד את הקבצים הבינריים (קובצי APK), וההנחיה
AllowedAPKSigningKeys
כדי להבטיח שנעשה שימוש במפתח החתימה הצפוי. במקרה זה, F-Droid לעולם לא ינסה
לפרסם קובצי APK חתומים על ידי F-Droid. אם fdroid publish מצליח לאמת שה־APK
שהורד תואם לזה שנבנה ממתכון ה־fdroiddata, ה־APK שהורד יפורסם. אחרת,
F-Droid ידלג על פרסום גרסה זו של היישומון.
פרסום קובצי APK חתומים הן על ידי המפתחים (מפתחי המקור) והן על ידי F-Droid
גישה זו מאפשרת לפרסם גם קובצי APK חתומים על ידי המפתחים (מפתחי המקור) וגם קובצי APK חתומים על ידי F-Droid. כך אפשר לספק עדכונים למשתמשים שהתקינו יישומונים ממקורות אחרים מלבד F-Droid (למשל Play Store), ובמקביל לספק עדכונים ליישומונים שנבנו ונחתמו על ידי F-Droid.
הדבר מחייב לחלץ את חתימות המפתחים (מפתחי המקור) ולהוסיף אותן ל־fdroiddata. לאחר מכן החתימות האלה מועתקות ל־APK הלא חתום שנבנה ממתכון ה־fdroiddata. אנו מספקים פקודה לחילוץ קל של חתימות מקובצי APK:
$ cd /path/to/fdroiddata
$ fdroid signatures F-Droid.apk
במקום קבצים מקומיים, אפשר גם להעביר ל־fdroid signatures כתובות HTTPS.
קובצי החתימה מחולצים לתיקיית נתוני העל של היישומון, מוכנים לשימוש עם fdroid
publish. חתימה מורכבת מ־2 עד 6 קבצים: חתימת v1 (manifest, קובץ חתימה וקובץ
בלוק חתימה) ו/או חתימת v2/v3 (APK Signing Block ו־offset); אם ה־APK נחתם
בחתימת v1 באמצעות, למשל, signflinger במקום apksigner, יהיה גם קובץ
differences.json. התוצאה של חילוץ חתימה תיראה בדומה לרשימות הקבצים האלה:
$ ls metadata/org.fdroid.fdroid/signatures/1000012/ # v1 signature only
CIARANG.RSA CIARANG.SF MANIFEST.MF
$ ls metadata/your.app/signatures/42/ # v1 + v2/v3 signature
APKSigningBlock APKSigningBlockOffset MANIFEST.MF YOURKEY.RSA YOURKEY.SF
כלים
השוואת ה־APK
אנו ממליצים להשתמש ב־diffoscope כדי למצוא בקלות
את ההבדלים בין ה־APK לייחוס שסיפק מפתח היישומון לבין ה־APK ש־fdroidserver
יצר.
את ה־APK ש־fdroidserver יצר אפשר למצוא, למשל, תחת
fdroiddata/build/com.example.app/app/build/outputs/apk/prod/release/example-1.0.0-prod-release-unsigned.apk
(בהרצה מקומית) או בתוצרי ה־pipeline (בשימוש ב־GitLab CI). יש להתאים את הנתיב
בהתאם (למשל, עבור גרסאות בנייה (flavors) שאינן prod).
תעדוף ותיקון הבדלים
HOWTO: diff & fix APKs for Reproducible Builds בוויקי של F-Droid מכיל מידע מפורט על סוגי ההבדלים השונים שנתקלים בהם בדרך כלל, באילו הבדלים כדאי בדרך כלל לטפל קודם בעת ניפוי שגיאות, ואיך לתקן בעיות נפוצות.
הוא גם מראה איך להשתמש בכלים ייעודיים שונים שעשויים לתת תוצאות טובות יותר
כאשר diffoscope אינו מספיק.
כלי APK לבניות שניתן לשחזר
הסקריפטים
מ־reproducible-apk-tools
(זמינים ב־fdroiddata כ־srclib) עשויים לעזור להפוך בניות לניתנות לשחזור,
למשל על ידי תיקון תווי שורה חדשה (CRLF לעומת LF) או הפיכת סדר ה־ZIP
לדטרמיניסטי, אם הסרת הגורם להבדלים אינה אפשרות מציאותית. בהתאם לפרטים, מפתחי
המקור צריכים להשתמש בסקריפטים אלה לפני החתימה על קובצי ה־APK שלהם, או שיש
להשתמש בהם במתכון ה־fdroiddata, או בשניהם.
disorderfs, שנוצר
במקור כדי להחדיר אי־דטרמיניזם לתהליכי בנייה, יכול גם לעשות את ההפך: להפוך את
הקריאה ממערכת הקבצים לדטרמיניסטית. במקרים מסוימים הדבר יכול להפוך, למשל, את
resources.arsc לניתן לשחזור. הנה דוגמה ממתכון קיים:
$ mv my.app my.app_underlying
$ disorderfs --sort-dirents=yes --reverse-dirents=no my.app_underlying my.app
מקורות אפשריים לבניות שאינן ניתנות לשחזור
יש דרכים שונות שבהן בניות עלולות שלא להיות ניתנות לשחזור. חלקן קלות יחסית להימנעות, ואחרות קשות לתיקון. ניסינו לרשום להלן כמה מקורות נפוצים.
יש גם את התקלה הזאת ב־GitLab.
באג: לבניות של Android Studio יש סדר ZIP לא דטרמיניסטי
Non-deterministic order of ZIP entries in APK makes builds not reproducible (ייתכן שנדרש חשבון Google לצפייה).
הערה: הבעיה אמורה להיות מתוקנת ב־Android Gradle plugin
(com.android.tools.build:gradle / com.android.application) 7.1.X
ואילך.
בעת בניית קובצי APK עם Android Studio, סדר רשומות ה־ZIP ב־APK עשוי להיות
שונה מזה של קובצי APK שנבנו בהפעלה ישירה של gradle, מה שפוגע בשחזוריות;
הסדר עשוי להיות לא דטרמיניסטי לחלוטין, ואף להשתנות בין בניות שונות של אותו
קוד מקור.
פתרון עוקף לגרסאות ישנות יותר הוא להפעיל את gradle ישירות (כמו בבניות של
F-Droid או של CI), תוך עקיפת Android Studio:
$ ./gradlew assembleRelease
הערה: בהתאם להגדרות החתימה שלך, ייתכן שיהיה צורך לחתום על ה־APK עם
apksigner לאחר מכן, מאחר ש־Android Studio אינו מבצע את החתימה במקרה זה.
apksigner מ־build-tools >= 35.0.0-rc1 מייצר קובצי APK שלא ניתן לאמת
שימוש ב־apksigner מ־build-tools בגרסה 34 מייצר קובצי APK ש־apksigcopier
יכול לאמת, אבל גרסאות חדשות יותר ייכשלו. אנו עוקבים אחר תקלה זו
ב־#3299 ויש מידע נוסף
ב־apksigcopier issues
105. תמונות Ubuntu של
Github Actions CI החל מיולי 2024 כוללות את גרסה 35, ולכן יש לבחור ידנית את
גרסת apksigner מ־34 במקום ברירת המחדל שבתבנית, „הגרסה האחרונה”.
באג: baseline.prof אינו דטרמיניסטי
לפעמים הקובץ baselin.prof אינו ניתן לשחזור. יש כמה פתרונות עוקפים אפשריים:
- להריץ את הבנייה שוב עד שהקובץ תואם.
- להשתמש באותו מספר ליבות מעבד כמו מפתחי המקור.
- להשבית את פרופיל הבסיס (baseline profile).
יש להוסיף את זה ל־
build.gradle:
tasks.whenTaskAdded { task ->
if (task.name.contains("ArtProfile")) {
task.enabled = false
}
}
או את זה ל־build.gradle.kts:
tasks.whenTaskAdded {
if (name.contains("פרופיל אומנותי")) {
enabled = false
}
}
תקלה: baseline.profm אינו דטרמיניסטי
Non-stable
assets/dexopt/baseline.profm
(ייתכן שנדרש חשבון Google לצפייה).
ראו גם סיכום זה של פתרונות עוקפים.
באג: coreLibraryDesugaring אינו דטרמיניסטי
הערה: הבעיה אמורה להיות מתוקנת ב־R8 (com.android.tools:r8) 3.0.69
ואילך.
במקרים מסוימים בניות אינן ניתנות לשחזור בגלל באג
ב־coreLibraryDesugaring
(ייתכן שנדרש חשבון Google לצפייה); באג זה השפיע על
NewPipe.
באג: הבדלי סוף שורה בין בניות ב־Windows וב־Linux
Newline differences between building on Windows vs Linux make builds not reproducible (ייתכן שנדרש חשבון Google לצפייה).
פתרון עוקף הוא להריץ את
fix-newlines.py
על ה־APK הלא חתום עם סופי השורה ה„שגויים” כדי לשנות אותם מ־LF ל־CRLF (או
להפך, עם --from-crlf), ולאחר מכן להריץ עליו שוב zipalign.
מקביליות: השחזוריות עשויה להיות תלויה במספר המעבדים/הליבות
הדבר יכול להשפיע על קובצי .dex (אף שנראה שזה נדיר) או על קוד מקומי
(native), למשל Rust.
שימוש במעבד/ליבה אחת בלבד כפתרון עוקף:
export CPUS_MAX=1
export CPUS=$(getconf _NPROCESSORS_ONLN)
for (( c=$CPUS_MAX; c<$CPUS; c++ )) ; do echo 0 > /sys/devices/system/cpu/cpu$c/online; done
הערה: פתרון עוקף זה משפיע על המכונה כולה, ולכן מומלץ להשתמש בו במכונה וירטואלית או במכולה שאינן קבועות.
עבור קוד Rust, אפשר להגדיר codegen-units =
1.
יש גם את התקלה הזאת ב־GitLab.
נתיבי בנייה מוטמעים
נתיבי בנייה מוטמעים הם
מקור לבעיות שחזוריות שמשפיעות על יישומונים שנבנו, למשל, עם Flutter,
python-for-android, או שמשתמשים בקוד מקומי (native), למשל Rust, C/C++, או
כל סוג של libfoo.so. יישומונים שנכתבו כולם ב־Java ו/או Kotlin נוטים שלא
להיות מושפעים.
לעיתים קרובות, הפתרון הקל ביותר הוא להשתמש תמיד באותה תיקיית עבודה בזמן
הבנייה; למשל /builds/fdroid/fdroiddata/build/your.app.id (ה־CI של
F-Droid), /home/vagrant/build/your.app.id (שרת הבנייה של F-Droid),
/tmp/build, או ליצור תיקייה שתשקף את התיקיות שבהן משתמשים מפתחי המקור,
למשל עבור macOS /Users/runner.
הערה: שימוש בתיקיית משנה של /tmp, שכל המשתמשים יכולים לכתוב אליה, עלול
להיות בעל השלכות אבטחה (במערכות מרובות משתמשים).
אם נתיב ה־SDK מוטמע בסופו של דבר ב־Flutter, אפשר להעביר את ה־SDK
לנתיב זה במתכון ולהגדיר אותו באמצעות: flutter config --android-sdk <path>,
מאחר שהגדרת ANDROID_SDK_ROOT עשויה שלא להספיק.
אם הספרייה לא עברה הסרת סמלים (strip) כראוי, ייתכן שיישאר בה מידע ניפוי
שגיאות, שבדרך כלל מכיל נתיבים רבים. הפעלת strip יכולה להסיר אותם. אפשר לעשות
זאת על ידי הגדרה נכונה של גרסת ה־NDK או העברת -s למקשר (linker). אפשר
לעשות זאת גם ידנית, למשל באמצעות llvm-strip.
חותמות זמן מוטמעות
חותמות זמן מוטמעות הן המקור הנפוץ ביותר לבעיות שחזוריות, ועדיף להימנע מהן.
הסרת סמלים (stripping) מספריות מקומיות
נראה שהסרת סמלים מספריות מקומיות (native), למשל libfoo.so, עלולה לגרום
לבעיות שחזוריות לסירוגין. חשוב להשתמש בגרסת ה־NDK המדויקת בעת הבנייה מחדש,
למשל r21e. השבתת הסרת הסמלים יכולה לעזור לפעמים. נראה ש־Gradle מסיר סמלים
מספריות משותפות כברירת מחדל, גם כאשר היישומון מקבל את הספריות המשותפות דרך
ספריית AAR. כך משביתים זאת ב־Gradle:
android {
packagingOptions {
doNotStrip '**/*.so'
}
}
build-id של NDK
במכונות בנייה שונות נעשה שימוש בנתיבי NDK שונים ובנתיבים שונים למיזם (ולכן
גם לתיקיית ה־jni שלו). הדבר מוביל לנתיבים שונים לקובצי המקור בסמלי ניפוי
השגיאות, וגורם למקשר (linker) לייצר build-id שונה, שנשמר גם לאחר הסרת
הסמלים.
פתרון אפשרי אחד הוא להעביר למקשר את --build-id=none, מה שישבית לחלוטין את
יצירת ה־build-id.
סגנון גיבוב (hash style) של NDK
LLVM מעביר ברירות מחדל שונות למקשרים בפלטפורמות שונות. לאחר ש־קומיט
זה
מוזג ל־NDK, ב־Debian ייעשה שימוש כברירת מחדל ב־--hash-style=gnu. כדי לשנות
את סגנון הגיבוב, אפשר להעביר למקשר את --hash-style=gnu.
מחרוזת הגרסה של clang מה־NDK בסעיף .comment
החל מ־NDK r26, מחרוזת הגרסה של Clang בסעיף ה־comment שונה בבנייה ב־MacOS וב־Linux, ונראית כך
Android (12027248, +pgo, -bolt, +lto, +mlgo, based on r522817) clang version 18.0.1 (https://android.googlesource.com/toolchain/llvm-project d8003a456d14a3deb8054cdaa529ffbf02d9b262)
בגלל מיטובים שונים שמופעלים בפלטפורמות שונות. אפשר להסיר את כל סעיף ה־.comment באמצעות
objcopy --remove-section .comment <file>
גרסאות (revisions) של platform
בשנת 2014, כלי ה־SDK של Android
שונו כך שיוסיפו
שני
רכיבי
נתונים
ב־AndroidManifest.xml כחלק מתהליך הבנייה: platformBuildVersionName
ו־platformBuildVersionCode. platformBuildVersionName כולל את ה„גרסה”
(revision) של חבילת ה־platform שמולה נבנה היישומון (למשל android-23),
אולם אי אפשר להתקין במקביל „גרסאות” שונות של אותה חבילת platform. בנוסף,
כלי ה־SDK אינם תומכים בציון הגרסה הנדרשת כחלק מתהליך הבנייה. לעיתים קרובות
התוצאה היא בנייה שהייתה ניתנת לשחזור, אלמלא ההבדל היחיד במאפיין
platformBuildVersionName.
ה־platform הוא החלק ב־SDK של Android שמייצג את הספרייה הסטנדרטית שמותקנת
בטלפון. לגרסה שלו יש שני חלקים: „קוד הגרסה” (version code), שהוא מספר שלם
שמייצג את מהדורת ה־SDK, וה„גרסה” (revision), שמייצגת גרסאות תיקוני באגים לכל
פלטפורמה. אפשר לראות את הגרסאות האלה בקובץ build.prop הכלול. לכל revision
יש מספר שונה ב־ro.build.version.incremental. ב־Gradle אין דרך לציין את
ה־revision ב־compileSdkVersion או ב־targetSdkVersion. אפשר להתקין רק
platform-23 אחד בכל פעם, בניגוד ל־build-tools, שבו אפשר להתקין כל מהדורה
במקביל.
הנה שתי דוגמאות שבהן החשד הוא שכל ההבדלים נובעים מגרסאות (revisions) שונות של הפלטפורמה:
- https://verification.f-droid.org/de.nico.ha_manager_25.apk.diffoscope.html
- https://verification.f-droid.org/de.nico.asura_12.apk.diffoscope.html
PNG Crush/Crunch
חלק סטנדרטי מתהליך הבנייה של Android הוא הרצת כלי כלשהו למיטוב PNG, כמו
aapt singleCrunch, pngcrush, zopflipng או optipng. כלים אלה אינם
מפיקים פלט דטרמיניסטי, והסיבה לכך עדיין אינה ברורה. מאחר שקובצי PNG נשמרים
בדרך כלל במאגר קוד המקור, פתרון עוקף לבעיה זו הוא להריץ את הכלי המועדף על
קובצי ה־PNG, ואז לבצע קומיט של השינויים למאגר קוד המקור (למשל git). לאחר
מכן, יש להשבית את תהליך מיטוב ה־PNG שבברירת המחדל על ידי הוספת הקוד הבא
ל־build.gradle:
android {
aaptOptions {
cruncherEnabled = false
}
}
יש לשים לב שכלים כמו svgo יכולים לבצע מיטוב דומה לקובצי SVG.
קובצי PNG שנוצרים מגרפיקה וקטורית (vector drawables)
Android Gradle plugin יוצר משאבי PNG מגרפיקה וקטורית עבור גרסאות Android ישנות. לרוע המזל, קובצי ה־PNG שנוצרים אינם ניתנים לשחזור.
אפשר להשבית את יצירת קובצי ה־PNG על ידי הוספת הקוד הבא ל־build.gradle:
android {
defaultConfig {
vectorDrawables.generatedDensities = []
}
}
ממטב R8
נראה שחלק מהמיטובים של R8 אינם דטרמיניסטיים, ומפיקים bytecode שונה בהרצות בנייה שונות.
לדוגמה, R8 מנסה למטב את השימוש ב־ServiceLoader על ידי יצירת רשימה סטטית של
כל השירותים בקוד. הסדר של רשימה זו עשוי להיות שונה (או אף לא שלם) בכל הרצת
בנייה. הדרך היחידה להימנע מהתנהגות זו היא להשבית מיטובים כאלה על ידי הצהרה
על המחלקות הממוטבות ב־proguard-rules.pro:
-keep class kotlinx.coroutines.CoroutineExceptionHandler
-keep class kotlinx.coroutines.internal.MainDispatcherFactory
יש להיזהר עם R8. תמיד יש לבדוק את הבניות כמה פעמים ולהשבית מיטובים שמפיקים פלט לא דטרמיניסטי.
אם ה־bytecode של DEX שונה ותלוי במספר ליבות המעבד, כדאי לנסות לעדכן את R8 לגרסה 8.6.33, 8.7.20, 8.8 או חדשה יותר, מאחר שכמה בעיות בתחום זה תוקנו.
מחלקות DEX בסדר שגוי
גם אם התוכן תואם, אם שמות קובצי המחלקות מתחלפים, השחזוריות תיכשל. הבעיה תוקנה עבור bundles ב־AGP 8.8, אבל ראינו אותה גם בקובצי APK. כדאי לנסות קודם עם AGP חדש יותר.
מכווץ המשאבים (Resource Shrinker)
אפשר להקטין את גודל קובץ ה־APK על ידי הסרת משאבים שאינם בשימוש מהחבילה. הדבר שימושי כאשר מיזם תלוי בספריות מנופחות כמו AppCompat, במיוחד כאשר נעשה שימוש בכיווץ קוד של R8/ProGuard.
עם זאת, ייתכן שמכווץ המשאבים יגדיל את גודל ה־APK בפלטפורמות שונות, במיוחד אם אין הרבה משאבים לכווץ, ואז ייעשה שימוש ב־APK המקורי במקום בזה המכווץ (התנהגות לא דטרמיניסטית של תוסף Gradle). יש להימנע משימוש במכווץ המשאבים, אלא אם הוא מקטין את גודל קובץ ה־APK באופן משמעותי.
מידע VCS
החל מ־Android Gradle Plugin 8.3, מידע VCS נוצר כברירת מחדל ונארז בתוך ה־apk
ב־META-INF/version-control-info.textproto, למשל
repositories {
system: GIT
local_root_path: "$PROJECT_DIR"
revision: "3a443877cd53e37d85cbc52adc8cfd558919d373"
}
אנו מבינים שמפתחים בונים ובודקים במהלך זרימת העבודה הרגילה שלהם, אך נא
להעלות קובצי APK של מהדורה שנבנו לאחר יצירת התגית, מעץ נקי בקומיט המתויג
עצמו (כלומר, ללא שינויים מקומיים או שאריות מבניות קודמות). רק במקרים חריגים,
כאשר אי אפשר לעשות זאת, יש להשבית את vcsInfo (מאחר שאחרת הדבר עלול לגרום
לבעיות), וזה נעשה כך:
buildTypes {
release {
vcsInfo.include false
}
}
נתוני על ב־ZIP
קובצי APK משתמשים בתבנית הקבצים ZIP, שתוכננה במקור סביב מערכת הקבצים FAT של MSDOS. הרשאות קבצים של UNIX נוספו כהרחבה. קובצי APK זקוקים רק לתבנית ה־ZIP הבסיסית ביותר, ללא אף אחת מההרחבות. הרחבות אלה מוסרות לעיתים קרובות בתהליך חתימת המהדורה הסופי. אבל תהליך בניית ה־APK יכול להוסיף אותן. לדוגמה:
--- a2dp.Vol_137.apk
+++ sigcp_a2dp.Vol_137.apk
@@ -1,50 +1,50 @@
--rw---- 2.0 fat 8976 bX defN 79-Nov-30 00:00 AndroidManifest.xml
--rw---- 2.0 fat 1958312 bX defN 79-Nov-30 00:00 classes.dex
--rw---- 1.0 fat 78984 bx stor 79-Nov-30 00:00 resources.arsc
+-rw-rw-rw- 2.3 unx 8976 b- defN 80-000-00 00:00 AndroidManifest.xml
+-rw---- 2.4 fat 1958312 b- defN 80-000-00 00:00 classes.dex
+-rw-rw-rw- 2.3 unx 78984 b- stor 80-000-00 00:00 resources.arsc
שרשראות כלים לא תואמות
שרשראות כלים שונות עשויות להפיק קבצים בינריים שונים. מקרה נפוץ הוא כאשר נעשה שימוש ביותר מגרסה/הפצה אחת של JDK לבניית ה־APK. לפעמים אפילו Gradle עשוי לערבב גרסאות של JDK בבניית APK. כדי להימנע מבעיות כאלה, יש להסיר JDK שאינם בשימוש.
בהשוואת ה־APK יופיעו בקובצי ה־classes.dex רשומות כמו זו, למשל Java 17
לעומת Java 11:
- .annotation system Ldalvik/annotation/Signature;
- value = {
- "()V"
- }
- .end annotation
או כמו זו, למשל Java 17 לעומת Java 21:
- .annotation system Ldalvik/annotation/MethodParameters;
- accessFlags = {
- 0x8010
- }
- names = {
- null
- }
- .end annotation
גם גרסאות NDK שונות מפיקות קבצים בינריים שונים. בדרך כלל אפשר לזהות זאת דרך
נתוני העל בספריות המקומיות, למשל גרסת ה־LLD. עם זאת, החל מ־NDK r26d נצפתה
התנהגות מוזרה: לפעמים רק סעיפי ה־.shstrtab ב־ELF של הספריות המקומיות
משתנים כאשר ה־NDK מותקן. הספריות המקומיות עשויות להיבנות יחד עם היישומון או
להימשך ממאגר maven. אם AGP מגלה שה־NDK מותקן, הוא ישתמש ב־NDK כדי להסיר
סמלים מהספרייה המקומית, אבל למעשה הוא רק משבש את סעיף ה־.shstrtab שלה. יש
לבדוק בקפידה את הגדרת ה־NDK כדי לוודא שהיא תואמת להגדרה של מפתחי המקור, כולל
גרסת ה־NDK והאם AGP משתמש בו.
שרת הבנייה של F-Droid תומך כרגע בגרסאות ה־JDK הבאות:
- 25 מ־Debian Trixie
- 21 מ־Debian Trixie
- 17 מ־Debian Bookworm
- 11 מ־Debian Bullseye
- 8 מ־Debian Sid
יש לשים לב שגרסאות שאינן מגרסת ה־stable הנוכחית של Debian (Trixie) עלולות להפוך ללא זמינות עם הזמן, ולכן מומלץ לעדכן ככל האפשר.
תמיכה בגודלי דף של 16KB
החל מ־Android 15, Android תומכת במכשירים שמוגדרים להשתמש בגודל דף של 16KB (מכשירי 16KB). אם היישומון שלך משתמש בספריות NDK כלשהן, ישירות או בעקיפין דרך SDK, יהיה צורך לבנות אותו מחדש כדי שיעבוד במכשירי 16KB אלה. מידע נוסף כאן
הוראות לפי שפה
ספריות מקומיות עשויות להיבנות בכלים ובשפות שונים. אף שהן סובלות מבעיות דומות בבניות שניתן לשחזר, דרכי התיקון שונות. להלן כמה פתרונות ידועים:
ndk-build
אפשר להוסיף את LOCAL_CFLAGS += <compiler args> ואת LOCAL_LDFLAGS += -Wl,<linker args>
לקובצי Android.mk או ל־build.gradle/build.gradle.kts:
android {
defaultConfig {
externalNativeBuild {
ndkBuild {
arguments "LOCAL_CFLAGS+=<compiler args> LOCAL_LDFLAGS+=-Wl,<linker args>"
}
}
}
}
CMake
בגרסאות CMake החל מ־3.13, אפשר להוסיף את add_compile_options("<compiler args>") ואת
add_link_options(LINKER:<linker args>) ל־CMakeLists.txt באופן גלובלי, למשל כדי להסיר את build-id:
add_link_options("LINKER:--build-id=none")
פקודה זו פועלת רק על ספריות שנוספו לאחר הפעלתה, ולכן יש להפעיל אותה בתחילת
קובץ ה־CMake. בגרסאות CMake שקודמות ל־3.13, אפשר להשתמש במקום זאת
ב־target_compile_options(<target> PRIVATE <compiler args>)
וב־target_link_libraries(<target> LINKER:<linker args>) עבור כל יעד. אפשר גם להגדיר זאת בקובצי Gradle:
android {
defaultConfig {
externalNativeBuild.cmake {
cFlags "<compiler args> -Wl,<linker args>" // or
arguments "-DCMAKE_C_FLAGS=<compiler args> -DCMAKE_SHARED_LINKER_FLAGS=-Wl,<linker args>"
}
}
}
ואפשר להשתמש בזה גם כדי לשנות את ההתנהגות של ספריות מ־pub ביישומוני Flutter,
למשל עבור ה־pub jni:
import com.android.build.gradle.LibraryExtension
subprojects {
plugins.withId("com.android.library") {
if (name == "jni") {
extensions.configure<LibraryExtension>("android") {
defaultConfig {
externalNativeBuild {
cmake {
arguments += "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,--build-id=none"
}
}
}
}
}
}
}
אפשר להשתמש בדגל -ffile-prefix-map כדי להסיר נתיבי בנייה מוטמעים. אפשר
להוסיף אותו ישירות ל־CMakeLists.txt:
add_compile_options("-ffile-prefix-map=${CMAKE_CURRENT_SOURCE_DIR}=.")
או ב־build.gradle:
externalNativeBuild {
cmake {
cFlags "-ffile-prefix-map=${rootDir}=."
cppFlags "-ffile-prefix-map=${rootDir}=."
}
}
Golang
אפשר להוסיף ארגומנטים למקשר ל־CGO_LDFLAGS. ארגומנטים שימושיים נוספים שאפשר
להעביר ל־go build הם -ldflags="-buildid=", -trimpath (כדי להימנע
מנתיבי בנייה מוטמעים) ו־-buildvcs=false.
Rust
אפשר להוסיף ארגומנטים למהדר ולמקשר ל־Cargo build.rustflags
ול־rustc Codegen Options. אפשר להוסיף ארגומנטים
למקשר באמצעות -C link-args=-Wl,<linker args>, ואפשר להוסיף את --remap-path-prefix=<old>=<new> כדי להסיר נתיבי בנייה.
ל־Cargo נוספה תכונה לא יציבה, trim-paths, שמסירה את כל הנתיבים המוחלטים ב־rust.
יש לקבע את שרשרת הכלים של Rust לאותה גרסה כמו אצל מפתחי המקור. אפשר לעשות זאת בעת התקנת rustup באמצעות rustup-init.sh -y --default-toolchain <version>.
כאשר ה־crate openssl משתמש בבניית OpenSSL מוטמעת (vendored), יש להגדיר את
ספריית OpenSSL באופן מיוחד כדי שתהיה ניתנת לשחזור. אפשר להגדיר את
SOURCE_DATE_EPOCH כדי להסיר את חותמות הזמן המוטמעות, ואת
CARGO_TARGET_DIR לנתיב מוחלט, למשל /tmp/build, כדי שהנתיב המוטמע יהיה
זהה במכונות שונות. גם ה־NDK צריך להיות באותו נתיב, ואפשר לפתור זאת על ידי
קישור שלו לאותו נתיב.
גם לנתיב CARGO_HOME יש תפקיד חשוב, והוא מגיע בסופו של דבר לספריות הבנויות;
מומלץ שיהיה זהה בין בניות, למשל לייצא אותו לפני הרצת rustup או כל פקודת
בנייה אחרת, ולא לשכוח לטעון (source) את env ממנו.
הוראות לפי ספרייה
ספריות מסוימות מייצרות קוד לא דטרמיניסטי בגלל חותמות זמן, איטרציות לא ממוינות וכו’. כמה תיקונים ידועים מתועדים להלן:
התוסף AboutLibraries Gradle plugin
אפשר למנוע מתוסף זה (com.mikepenz.aboutlibraries.plugin) להוסיף חותמת זמן
לקובץ ה־JSON שהוא יוצר על ידי הוספת הקוד הבא ל־build.gradle:
aboutLibraries {
// Remove the "generated" timestamp to allow for reproducible builds
excludeFields = ["generated"]
}
עבור build.gradle.kts, יש להוסיף במקום זאת את הקוד הבא:
aboutLibraries {
// Remove the "generated" timestamp to allow for reproducible builds
excludeFields = arrayOf("generated")
}
EventBus
הוא מייצר קוד לא דטרמיניסטי שאפשר למיין לאחר יצירת המחלקות. הוראות מפורטות אפשר למצוא ב־קוד המקור של Eternity.
הסבה ל־reproducible builds
משימות פתוחות
- סדר המיון של jar בקובצי APK
- גרסאות שונות של
aaptמפיקות תוצאות שונות (XML ושמות של תיקיות משנה ב־res/)
מקורות
- https://gitlab.com/fdroid/fdroidserver/commit/8568805866dadbdcc6c07449ca6b84b80d0ab03c
- שרת האימות
- https://verification.f-droid.org
- https://reproducible-builds.org
- https://wiki.debian.org/ReproducibleBuilds
- https://gitian.org/
- Google Issue #70292819 platform-27_r01.zip was overwritten with a new update
- Google Issue #37132313 platformBuildVersionName makes builds difficult to reproduce, creates unneeded diffs
- Google Issue #110237303 resources.arsc built with non-determism, prevents reproducible APK builds
- Unreproducible/non-deterministic code generation by navigation.safeargs.kotlin
- unneeded DEX code differences based on number of CPUs used in build process
- Assembling the bundle with AssembleBundleTask produces DEX files in the wrong order, resulting in a checksum mismatch
- differing DEX bytecode depending on the number of CPU cores breaks Reproducible Builds
- unneeded DEX code differences based on number of CPUs used in build process