בניות שניתן לשחזר

מבוא

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 אינו ניתן לשחזור. יש כמה פתרונות עוקפים אפשריים:

  1. להריץ את הבנייה שוב עד שהקובץ תואם.
  2. להשתמש באותו מספר ליבות מעבד כמו מפתחי המקור.
  3. להשבית את פרופיל הבסיס (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) שונות של הפלטפורמה:

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/)

מקורות