Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reproducible Builds #140

Closed
IzzySoft opened this issue Nov 24, 2024 · 44 comments
Closed

Reproducible Builds #140

IzzySoft opened this issue Nov 24, 2024 · 44 comments

Comments

@IzzySoft
Copy link

Your app was requested for inclusion with the IzzyOnDroid repo.

At IzzyOnDroid we support Reproducible Builds (see: Reproducible Builds, special client support and more at IzzyOnDroid). Trying for yours, I was able to successfully generate the APK using ./gradlew assembleRelease, but the resulting APKs were not identical. The APK was obviously not built from a clean tree: the embedded commit hash suggests a commit before the tag, but the APK has versionName and versionCode with values committed with the tag. So obviously there were some local changes made after the first commit, then the APK was built, and then there might have been additional changes before the next commit:

  -rw-r--r--  0.0 unx       56 b-       52 defN 1981-01-01 01:01:02 32137644 META-INF/com/android/build/gradle/app-metadata.properties
- -rw-r--r--  0.0 unx      120 b-      117 defN 1981-01-01 01:01:02 dab95447 META-INF/version-control-info.textproto
- -rw-r--r--  0.0 unx     2660 b-     2660 stor 1981-01-01 01:01:02 a5a64965 assets/dexopt/baseline.prof
- -rw-r--r--  0.0 unx      189 b-      189 stor 1981-01-01 01:01:02 c16b9473 assets/dexopt/baseline.profm
- -rw-r--r--  0.0 unx  9284468 b-  3467412 defN 1981-01-01 01:01:02 5dd0c4e1 classes.dex
+ -rw-r--r--  0.0 unx      120 b-      117 defN 1981-01-01 01:01:02 290f383c META-INF/version-control-info.textproto
+ -rw-r--r--  0.0 unx     2666 b-     2666 stor 1981-01-01 01:01:02 f9d5500c assets/dexopt/baseline.prof
+ -rw-r--r--  0.0 unx      189 b-      189 stor 1981-01-01 01:01:02 19d3f940 assets/dexopt/baseline.profm
+ -rw-r--r--  0.0 unx  9282736 b-  3466876 defN 1981-01-01 01:01:02 66b0b0ed classes.dex
  -rw-r--r--  0.0 unx     2468 b-      999 defN 1981-01-01 01:01:02 afaef08f classes2.dex

A diff of the classes.dex seems to confirm that (that diff is rather big, so I did not include it here).

We'd appreciate if you could help making your build reproducible. We've prepared some hints on reproducible builds for that.

Looking forward to your reply!

@BlackyHawky
Copy link
Owner

@IzzySoft
I took a look at the recommendations for producing Reproducible Builds.

In these recommendations, it seems that some do not concern the application:

I have some questions about some parts of these recommendations:

  • Clean Builds
    • How to automate this? Should this ./gradlew clean assembleRelease --no-build-cache --no-configuration-cache --no-daemon code be indicated somewhere?
  • JDK versions
    • In my build.gradle file it says:
      compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 }
      In Android Studio settings, the Gradle JDK points to GRADLE_LOCAL_JAVA_HOME JetBrains Runtime 21.0.3 C:\Users… …
      Is this a problem?

What I can do today (although I'm not sure if applying only this will solve the problem 🤔):

@IzzySoft
Copy link
Author

Nice, thanks!

Clean builds: if you use CI to build the app from the (tagged) commit, that should take care for it (wouldn't it usually run in a clean container?). If you build locally: yes, that's almost exactly the command we use with "flaky builds" (those with non-deterministic outcome, producing the "matching" result maybe every 3rd or 5th run, so we let the build run in circles until that "matching" result is achieved): making sure it's a clean tree and no artifacts remaining from previous builds are used. I've heard the equivalent in Android Studio would be running "clean" before "build".

JDK: your build.gradle specifies the minimum version needed: source requires at least JDK-17 to build, results need at least JRE 17 to run. So both conditions are met by JDK-21. Different JDK versions (the one you use and the one used by the rebuilder for your app) can produce different results, which is why we try to keep that in sync (and check with you if you might have upgraded when we get different results). And indeed, my build was done with JDK-17 – but that would not explain such a huge difference. But testing it again: the result is indeed different, though that doesn't change the RB status here as the source is different, too (see "local changes" above: your APK of 2.10 was not built from the commit the tag points to… but we're talking of old stuff here, I see there's a new release so let me check that now, with JDK set to 21.

Compressing Images: going by the above diff, you don't seem affected by that (graphics show separately usually – I'm not aware of graphics inside classes.dex.


Now let's see for 2.10.11:

  -rw-r--r--  0.0 unx      120 b-      118 defN 1981-01-01 01:01:02 8a77e52d META-INF/version-control-info.textproto
- -rw-r--r--  0.0 unx     2668 b-     2668 stor 1981-01-01 01:01:02 3bb5d7e3 assets/dexopt/baseline.prof
+ -rw-r--r--  0.0 unx     2668 b-     2668 stor 1981-01-01 01:01:02 abd4a72f assets/dexopt/baseline.prof
  -rw-r--r--  0.0 unx      189 b-      189 stor 1981-01-01 01:01:02 0445e85e assets/dexopt/baseline.profm
- -rw-r--r--  0.0 unx  9278084 b-  3465238 defN 1981-01-01 01:01:02 eb20df91 classes.dex
+ -rw-r--r--  0.0 unx  9278088 b-  3465214 defN 1981-01-01 01:01:02 b82e816d classes.dex
  -rw-r--r--  0.0 unx     2468 b-      999 defN 1981-01-01 01:01:02 afaef08f classes2.dex

OK, embedded commit hash this time matches, classes.dex again is different. Dex diff is much smaller, though. Relevant part (just stripping "head and foot"):

-|: new-instance v2, Lcom/best/deskclock/widget/EmptyViewController;
-|: iget-object v3, v5, Lcom/best/deskclock/AlarmClockFragment;.mMainLayout:Landroid/view/ViewGroup;
-|: iget-object v4, v5, Lcom/best/deskclock/AlarmClockFragment;.mRecyclerView:Landroidx/recyclerview/widget/RecyclerView;
-|: invoke-direct {v2, v3, v4, v0}, Lcom/best/deskclock/widget/EmptyViewController;.<init>:(Landroid/view/ViewGroup;Landroid/view/View;Landroid/view/View;)V
-|: iput-object v2, v5, Lcom/best/deskclock/AlarmClockFragment;.mEmptyViewController:Lcom/best/deskclock/widget/EmptyViewController;
+|: new-instance v0, Lcom/best/deskclock/widget/EmptyViewController;
+|: iget-object v2, v5, Lcom/best/deskclock/AlarmClockFragment;.mMainLayout:Landroid/view/ViewGroup;
+|: iget-object v3, v5, Lcom/best/deskclock/AlarmClockFragment;.mRecyclerView:Landroidx/recyclerview/widget/RecyclerView;
+|: iget-object v4, v5, Lcom/best/deskclock/AlarmClockFragment;.mAlarmsEmptyView:Landroid/widget/TextView;
+|: invoke-direct {v0, v2, v3, v4}, Lcom/best/deskclock/widget/EmptyViewController;.<init>:(Landroid/view/ViewGroup;Landroid/view/View;Landroid/view/View;)V
+|: iput-object v0, v5, Lcom/best/deskclock/AlarmClockFragment;.mEmptyViewController:Lcom/best/deskclock/widget/EmptyViewController;
 |: new-instance v0, Lcom/best/deskclock/alarms/AlarmTimeClickHandler;
 |: iget-object v2, v5, Lcom/best/deskclock/AlarmClockFragment;.mAlarmUpdateHandler:Lcom/best/deskclock/alarms/AlarmUpdateHandler;
 |: invoke-direct {v0, v5, v8, v2}, Lcom/best/deskclock/alarms/AlarmTimeClickHandler;.<init>:(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Lcom/best/deskclock/alarms/AlarmUpdateHandler;)V
@@ -891519,10 +891520,10 @@
       catches       : (none)
       positions     :
       locals        :
-        0x0000 - 0x00e7 reg=5 this Lcom/best/deskclock/AlarmClockFragment;
-        0x0000 - 0x00e7 reg=6 (null) Landroid/view/LayoutInflater;
-        0x0000 - 0x00e7 reg=7 (null) Landroid/view/ViewGroup;
-        0x0000 - 0x00e7 reg=8 (null) Landroid/os/Bundle;
+        0x0000 - 0x00e9 reg=5 this Lcom/best/deskclock/AlarmClockFragment;
+        0x0000 - 0x00e9 reg=6 (null) Landroid/view/LayoutInflater;
+        0x0000 - 0x00e9 reg=7 (null) Landroid/view/ViewGroup;
+        0x0000 - 0x00e9 reg=8 (null) Landroid/os/Bundle;

I'm not sure what to make out of that: looks like the same "code" but assigned a different register? Btw: Building with JDK-17, the DexDiff gets much larger again, so JDK-21 already seems the best choice here at our side. Did you build this with Android Studio? Can you choose OpenJDK there instead of Jetbrains (just to check if that makes a difference, as it sometimes does with different "vendors")?

@BlackyHawky
Copy link
Owner

@IzzySoft
You're welcome.

JDK:

your APK of 2.10 was not built from the commit the tag points to…

I am very surprised that you say this because I always publish the application after the commit named "Update gradle, translations, version and fastlane" (or something very similar)

Did you build this with Android Studio?

Yes I did. I am using version 2024.2.1 Patch 2.

Can you choose OpenJDK there instead of Jetbrains

I can't currently do this because I'm using my company computer which prevents me from installing a lot of software.
As soon as I have repaired my personal computer (it will be done this week) I will provide you with an apk having taken care to choose Open-JDK and having launched "Clean" before "Build".

@IzzySoft
Copy link
Author

I am very surprised that you say this

Well, that's what the APK said. There's embedded version info (see the diff: META-INF/version-control-info.textproto) containing the commit hash. And the hash contained there was not the one the tag pointed to. 🤷‍♂️

I can't currently do this because I'm using my company computer which prevents me from installing a lot of software.
As soon as I have repaired my personal computer (it will be done this week) I will provide you with an apk having taken care to choose Open-JDK and having launched "Clean" before "Build".

Fair enough, thanks a lot! I cannot even tell if the Jetbrain SDK is causing a difference there. But if we see nothing else, we need to rule that out (sometimes different SDKs cause differences).

@BlackyHawky
Copy link
Owner

@IzzySoft
Hi,
I didn't forget your request but some unforeseen circumstances prevented me from repairing my computer.
I should have time today to do so and should be able to provide you with an apk using Open-JDK.

Well, that's what the APK said. There's embedded version info (see the diff: META-INF/version-control-info.textproto) containing the commit hash. And the hash contained there was not the one the tag pointed to. 🤷‍♂️

You're right... I must have been really tired for that to happen 😬

@IzzySoft
Copy link
Author

IzzySoft commented Dec 3, 2024

I must have been really tired for that to happen

I do know exactly what you mean – am there too often myself…

I should have time today to do so and should be able to provide you with an apk using Open-JDK.

Wonderful, looking forward to that, thanks! And on the risk of repeating myself: Please remember running "clean" before "build" in Studio. Studio tends to reuse artifacts from prior runs otherwise, I've been told.

@BlackyHawky
Copy link
Owner

@IzzySoft
I was finally able to do what you asked me; sorry again for taking so long.

As you can see on the screenshot below, I am using Open-JDK 17:

And below, an APK where I took care to do "Clean" before "Build" (This file was built from the the current latest commit e00a0d7):

Clock_2.10.1-release.zip

@IzzySoft
Copy link
Author

IzzySoft commented Dec 3, 2024

Hm, seems to be prety much the same. APK diff:

  -rw-r--r--  0.0 unx      120 b-      117 defN 1981-01-01 01:01:02 cd3e6b68 META-INF/version-control-info.textproto
- -rw-r--r--  0.0 unx     2651 b-     2651 stor 1981-01-01 01:01:02 2b9fd0d4 assets/dexopt/baseline.prof
+ -rw-r--r--  0.0 unx     2651 b-     2651 stor 1981-01-01 01:01:02 35a31afb assets/dexopt/baseline.prof
  -rw-r--r--  0.0 unx      189 b-      189 stor 1981-01-01 01:01:02 19d3f940 assets/dexopt/baseline.profm
- -rw-r--r--  0.0 unx  9281136 b-  3465405 defN 1981-01-01 01:01:02 45b2cf3e classes.dex
+ -rw-r--r--  0.0 unx  9281144 b-  3465394 defN 1981-01-01 01:01:02 4236ef57 classes.dex
  -rw-r--r--  0.0 unx     2468 b-      999 defN 1981-01-01 01:01:02 afaef08f classes2.dex

And the dex:

 |: iput-object v0, v5, Lcom/best/deskclock/AlarmClockFragment;.mMainLayout:Landroid/view/ViewGroup;
-|: new-instance v2, Lcom/best/deskclock/alarms/AlarmUpdateHandler;
-|: iget-object v3, v5, Lcom/best/deskclock/AlarmClockFragment;.mContext:Landroid/content/Context;
-|: invoke-direct {v2, v3, v5, v0}, Lcom/best/deskclock/alarms/AlarmUpdateHandler;.<init>:(Landroid/content/Context;Lcom/best/deskclock/alarms/ScrollHandler;Landroid/view/ViewGroup;)V
-|: iput-object v2, v5, Lcom/best/deskclock/AlarmClockFragment;.mAlarmUpdateHandler:Lcom/best/deskclock/alarms/AlarmUpdateHandler;
+|: new-instance v0, Lcom/best/deskclock/alarms/AlarmUpdateHandler;
+|: iget-object v2, v5, Lcom/best/deskclock/AlarmClockFragment;.mContext:Landroid/content/Context;
+|: iget-object v3, v5, Lcom/best/deskclock/AlarmClockFragment;.mMainLayout:Landroid/view/ViewGroup;
+|: invoke-direct {v0, v2, v5, v3}, Lcom/best/deskclock/alarms/AlarmUpdateHandler;.<init>:(Landroid/content/Context;Lcom/best/deskclock/alarms/ScrollHandler;Landroid/view/ViewGroup;)V
+|: iput-object v0, v5, Lcom/best/deskclock/AlarmClockFragment;.mAlarmUpdateHandler:Lcom/best/deskclock/alarms/AlarmUpdateHandler;
 |: sget v0, Lcom/best/deskclock/R$id;.alarms_empty_view:I
 |: invoke-virtual {v7, v0}, Landroid/view/View;.findViewById:(I)Landroid/view/View;
 |: move-result-object v0
 |: check-cast v0, Landroid/widget/TextView;
 |: iput-object v0, v5, Lcom/best/deskclock/AlarmClockFragment;.mAlarmsEmptyView:Landroid/widget/TextView;
-|: new-instance v2, Lcom/best/deskclock/widget/EmptyViewController;
-|: iget-object v3, v5, Lcom/best/deskclock/AlarmClockFragment;.mMainLayout:Landroid/view/ViewGroup;
-|: iget-object v4, v5, Lcom/best/deskclock/AlarmClockFragment;.mRecyclerView:Landroidx/recyclerview/widget/RecyclerView;
-|: invoke-direct {v2, v3, v4, v0}, Lcom/best/deskclock/widget/EmptyViewController;.<init>:(Landroid/view/ViewGroup;Landroid/view/View;Landroid/view/View;)V
-|: iput-object v2, v5, Lcom/best/deskclock/AlarmClockFragment;.mEmptyViewController:Lcom/best/deskclock/widget/EmptyViewController;
+|: new-instance v0, Lcom/best/deskclock/widget/EmptyViewController;
+|: iget-object v2, v5, Lcom/best/deskclock/AlarmClockFragment;.mMainLayout:Landroid/view/ViewGroup;
+|: iget-object v3, v5, Lcom/best/deskclock/AlarmClockFragment;.mRecyclerView:Landroidx/recyclerview/widget/RecyclerView;
+|: iget-object v4, v5, Lcom/best/deskclock/AlarmClockFragment;.mAlarmsEmptyView:Landroid/widget/TextView;
+|: invoke-direct {v0, v2, v3, v4}, Lcom/best/deskclock/widget/EmptyViewController;.<init>:(Landroid/view/ViewGroup;Landroid/view/View;Landroid/view/View;)V
+|: iput-object v0, v5, Lcom/best/deskclock/AlarmClockFragment;.mEmptyViewController:Lcom/best/deskclock/widget/EmptyViewController;
 |: new-instance v0, Lcom/best/deskclock/alarms/AlarmTimeClickHandler;
 |: iget-object v2, v5, Lcom/best/deskclock/AlarmClockFragment;.mAlarmUpdateHandler:Lcom/best/deskclock/alarms/AlarmUpdateHandler;
 |: invoke-direct {v0, v5, v8, v2}, Lcom/best/deskclock/alarms/AlarmTimeClickHandler;.<init>:(Landroidx/fragment/app/Fragment;Landroid/os/Bundle;Lcom/best/deskclock/alarms/AlarmUpdateHandler;)V
@@ -891542,10 +891544,10 @@
       catches       : (none)
       positions     :
       locals        :
-        0x0000 - 0x00e5 reg=5 this Lcom/best/deskclock/AlarmClockFragment;
-        0x0000 - 0x00e5 reg=6 (null) Landroid/view/LayoutInflater;
-        0x0000 - 0x00e5 reg=7 (null) Landroid/view/ViewGroup;
-        0x0000 - 0x00e5 reg=8 (null) Landroid/os/Bundle;
+        0x0000 - 0x00e9 reg=5 this Lcom/best/deskclock/AlarmClockFragment;
+        0x0000 - 0x00e9 reg=6 (null) Landroid/view/LayoutInflater;
+        0x0000 - 0x00e9 reg=7 (null) Landroid/view/ViewGroup;
+        0x0000 - 0x00e9 reg=8 (null) Landroid/os/Bundle;

I'm a bit clueless. Can you try ./gradlew assembleRelease at the command-line and see if sha256sum *.apk gives you a hash of 7b1380a980518cb0cfb950cf482935a65dc8152abd1981f8be7ea8c0411ab0b1 – just to rule out it's Studio making the difference here? That's the hash of the unsigned APK I get from my build with JDK-17 on debian:bookworm.

@BlackyHawky
Copy link
Owner

BlackyHawky commented Dec 3, 2024

@IzzySoft
Not sure if this is correct but I prefer to specify the method used to obtain the hash value.
In the directory where the apk is, I opened a command prompt and typed : CertUtil -hashfile Clock_2.10.1-release-unsigned.apk SHA256 (I use Windows).
So I get: fcc8c8dd2fff8dfd7d63ef8e66092e1f7da00425f19b59eeed3b9246936be6a7

Edit: Do you think that before running "Clean" and "Build" I should also do "Invalidate caches" in Android Studio? 🤔

@IzzySoft
Copy link
Author

IzzySoft commented Dec 4, 2024

Yeah, according to this it should be the corresponding Windows command. OK, that does NOT match then either. Now I'm out of clues. Very last straw to pull on: do you get the same has on an APK build from a fresh clode (i.e. checkout the repo in a different place, cd into it, run gradlew assembleRelease there)? That then should rule out any .gitignored files and anything else that might have slipped into the "original tree" – and ensure you build on the very same source that I got.

I have never used Android Studio myself, so all I know about it is "second hand", sorry – but cleaning "everything" should not hurt: cached stuff could indeed play in here, and if "clean" does not take care for that… worth a try.

@BlackyHawky
Copy link
Owner

BlackyHawky commented Dec 4, 2024

@IzzySoft
I'm afraid I didn't understand your request well.
If I run the command in another folder of the repository (for example in the "main" folder) and I run the command: gradlew assembleRelease here is the error message from the command prompt:

F:\ANDROID\DEVELOPER\STUDIOPROJECTS\Clock_BLACKYHAWKY\app\src\main>F:\ANDROID\DEVELOPER\STUDIOPROJECTS\Clock_BLACKYHAWKY\gradlew assembleRelease
Calculating task graph as no cached configuration is available for tasks: assembleRelease

FAILURE: Build failed with an exception.

* What went wrong:
Project directory 'F:\ANDROID\DEVELOPER\STUDIOPROJECTS\Clock_BLACKYHAWKY\app\src\main' is not part of the build defined by settings file 'F:\ANDROID\DEVELOPER\STUDIOPROJECTS\Clock_BLACKYHAWKY\settings.gradle'. If this is an unrelated build, it must have its own settings file.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 1s
Configuration cache entry stored.

I must have made a mistake and I apologize for it as I am not used to using this kind of command.

Finally, to clarify this topic, what should I get on my side to know if the Reproducible Builds are compliant? I'm a bit lost... 🤔

Edit 1: It seems that NewPipe has managed to do the Reproducible Build. Do you have any idea what is different compared to our tests?

Edit 2: In the build.gradle file of the NewPipe app it is indicated as follows: shrinkResources false // disabled to fix F-Droid's reproducible build

Do you think I should do the same? But again, how do I know if it's ok without me asking you?

@IzzySoft
Copy link
Author

IzzySoft commented Dec 4, 2024

If I run the command in another folder

Nonono. OK, let me be more details. Assuming your code currently resides in C:\Users\BlackyHawky\Projects\Clock:

cd C:\Users\BlackyHawky\Projects
mkdir test
cd test
git clone -b 2.10.1 https://github.com/BlackyHawky/Clock.git
cd Clock
gradlew assembleRelease

I.e. check out a fresh copy of your project into a clean place (so you get exactly the same code I have here) and build from that. The above gives you a clean tree at the 2.10.1 tag. Then, tell me the sha256 hash of the resulting, unsigned APK.

To your "Edit 1": unfortunately, no (there it was just that LK built with JDK-17 while they had switched to 21, which our builders followed up to so we didn't have that issue). To "Edit: 2": that should not affect classes.dex but the contents of the res/ folder, AFAIK. And I guess, like PNG crunching, resource shrinking is not deterministic (i.e. it produces different results each time you run it) which is why it doesn't go well with RB. See also here for details; I've no idea why they had to explicitly disable it, it seems the default is "disabled".

how do I know if it's ok without me asking you?

I cannot tell. In many cases I, too, only know when I see the results…

@secretmango
Copy link

Question, are there issues for recent Android versions if apps are built with the lowest possible JDK?

@IzzySoft
Copy link
Author

IzzySoft commented Dec 4, 2024

To my knowledge, JDK should not be relevant for that (only targetSDK AFAIK). But at our builders, we can use JDKs 11, 17 and 21, so there'd be no problem going with 21 if you want to. Though it would be nice if you could confirm the APK checksum with JDK-17 and the "separate clone" first 😉

@BlackyHawky
Copy link
Owner

@IzzySoft
I will do the test tonight (Paris local time).
By the way, I have to do the test again because what I provided you did not point to the commit of version 2.10.1 (15a6c92) but to the last commit (e00a0d7); this is certainly why we find inconsistencies.

@IzzySoft
Copy link
Author

IzzySoft commented Dec 4, 2024

META-INF/version-control-info.textproto from my APK with the hash 7b1380a980518cb0cfb950cf482935a65dc8152abd1981f8be7ea8c0411ab0b1:

repositories {
  system: GIT
  local_root_path: "$PROJECT_DIR"
  revision: "e00a0d711f9a4aa606c0d43c5c72ee24c741d086"
}

Yes, built from e00a0d7, right. Mine too.

@IzzySoft
Copy link
Author

IzzySoft commented Dec 4, 2024

@BlackyHawky how many CPU cores do you use when building? This could have something to do with it (see https://issuetracker.google.com/issues/366412380). I've now run builds with 16, 8, 6, 4 and 2 cores, each produce a different APK (could as well be a non-deterministic build (aka "shaky build") in general, should maybe try our "unshaker", will report if that did something). Knowing your number-of-cores would increase our unshaking chances.

@BlackyHawky
Copy link
Owner

BlackyHawky commented Dec 4, 2024

@IzzySoft
I'm using 4 cores (Hyper-Threading disabled)

Edit: As stated in stackoverflow here, this can be set in the local.properties file.
Do you think this should be done?

If this has an impact I will wait before doing the tests again.

@IzzySoft
Copy link
Author

IzzySoft commented Dec 4, 2024

Thanks to having multiple builders now, I've utilized them both to run one with 4 and one with 8 cores at the same time (we can set the number of cores in our recipes). I've let the build run in a loop. Both produced 10 times the same hash, though none of them matching yours – and not matching each other. So it's definitely something with cores, but probably not only.

As stated in stackoverflow here, this can be set in the local.properties file.

Oh, nice – didn't know that! Looking at the official documantation at Gradle Build Environment Documentation:

org.gradle.parallel=(true,false): When configured, Gradle will fork up to org.gradle.workers.max JVMs to execute projects in parallel. Default is false.

and

org.gradle.workers.max=(max # of worker processes): When configured, Gradle will use a maximum of the given number of workers. Default is the number of CPU processors.

So the latter would make sense to be set to what your "official builds" have. But: Running with 4 cores, the diff here gets even bigger. Additionally to baseline.prof and classes.dex, also baseline.profm differs and the dex diff is twice as large. Same if I run with 8 cores. So this does not give us progress.

Was woth a try. Luckily I covered the number of cores you really used, so we can say that does not help – at least not that alone. Something else we might have missed?

And should you want to give those gradle settings a try: the hash of the unsigned APK for my builds were:

  • 7b1380a980518cb0cfb950cf482935a65dc8152abd1981f8be7ea8c0411ab0b1 (no core specification)
  • 04ad163e8ddb29f8702e5d1c4ff5c2ce0a17acb30700c88dbfde54d8cf76cd67 (8 cores)
  • 8c5bc45b09f62f71503c75dcc43235aaeb0aa093d19126aa0b06a1ce8b984044 (4 cores)
  • 3496e8a9c64f225aa8f1dc610ce33b9d03f6ba71c074b7e010dd6b67d7684af8 (2 or 6 cores, don't remember)
  • be252da84c44b7061fab77866cdf584dd561026e16ef8540160e9d91385b31da (6 or 2 cores I guess)

Tried to many, should have taken notes…

@IzzySoft
Copy link
Author

IzzySoft commented Dec 4, 2024

PS, if you want to try it in a loop to see if some setting hits that and if it always produces the same hash:

set +x
PROF_FILE=app/build/intermediates/binary_art_profile/release/compileReleaseArtProfile/baseline.prof
PROF_SHA1=6dc56cefc57875c10bdafb34c9ba4f4d4384a424
for _ in {1..10}; do
  ./gradlew clean assembleRelease --no-build-cache --no-configuration-cache --no-daemon
  if [ "$( sha1sum "$PROF_FILE" | cut -d' ' -f1 )" = "$PROF_SHA1" ]; then
    break
  fi
done

@BlackyHawky
Copy link
Owner

@IzzySoft
I think I have some good news for you.

If I apply this without ever opening Android Studio:

Nonono. OK, let me be more details. Assuming your code currently resides in C:\Users\BlackyHawky\Projects\Clock:

cd C:\Users\BlackyHawky\Projects
mkdir test
cd test
git clone -b 2.10.1 https://github.com/BlackyHawky/Clock.git
cd Clock
./gradlew assembleRelease

The hash you get is identical to the one you found with 4 cores.
8c5bc45b09f62f71503c75dcc43235aaeb0aa093d19126aa0b06a1ce8b984044

@BlackyHawky
Copy link
Owner

BlackyHawky commented Dec 5, 2024

@IzzySoft
I understood the difference in the hash value that there was between our 2 versions. It is simply the commit on which our apk pointed. (My created apks were pointing to the very latest commit I published instead of pointing to the commit of version 2.10.1 because I thought it was the same for you).

Indeed, in my default folder, if I apply from Android Studio ./gradlew assembleRelease after having done Clean, the hash I get is identical to the one you found with 4 cores (8c5bc45b09f62f71503c75dcc43235aaeb0aa093d19126aa0b06a1ce8b984044).

In summary, I think we can conclude:

  • Build with the same number of cores as my computer;
  • Do "Clean" before "Build";
  • Lastly (maybe) use Open-JDK 17;

Am I right, do you think?

Below is the apk if you need it:

Clock_2.10.1-release-unsigned.zip

@IzzySoft
Copy link
Author

IzzySoft commented Dec 5, 2024

Great to see we've solved the riddle! Full ack to your "marshal plan". May I suggest we do a test run for that, to get the shield up?

  • you prepare 2.10.2 release but do not tag it yet
  • build the APK from that commit as your "marshal plan" outlines (make sure there are no uncommitted changes in your tree (git status), JDK-17, use 4 cores, first "clean" then "build")
  • you attach the APK here and name the commit along
  • I use that to make an RB run, and let you know the results

If it works out, you'd just need to tag that commit, make it a release, and attach the APK to that. With that in place, I'd then establish Clock in my builder.

@BlackyHawky
Copy link
Owner

BlackyHawky commented Dec 6, 2024

@IzzySoft
I will do it tonight (or Sunday in the worst case) because I need my personal computer.

Quick questions:

  1. do you think it is necessary to add org.gradle.workers.max=4 in the "gradle.properties" file?
  2. do we agree that i can include the latest published commits (this is certainly what you meant in this sentence: "build the APK from that commit as your "marshal plan" outlines..." ; I want to be sure I understand your sentence correctly)?

@IzzySoft
Copy link
Author

IzzySoft commented Dec 6, 2024

Thanks! As for your points:

  1. I don't think it is necessary – I guess even when built with 32 cores it will produce a working APK. It might be worth a try, though, if that makes it RB without the need of specifying the number of cores in our recipes.
  2. Yes. What was meant is: "build from a clean tree at the commit (the tag points to)" (terms in parenthesis for the later release builds). APK and commit must be the same match on your side and ours. And "clean" because – well, as we just figured, right? 😉 It's also the "first basic rule" in our hints on reproducible builds btw.

@BlackyHawky
Copy link
Owner

@IzzySoft
Below is the apk of the new version:

Clock_2.11-release.zip

Commit: 77dace9b3e82de24a0674b975d047b69f85c1c50

Hash: 4cc2369392afa4b71fa1c197f7b617f14588f3785e0f88411f5c71b8e1342ca8

I'm keeping my fingers crossed that everything will run properly.

@IzzySoft
Copy link
Author

IzzySoft commented Dec 8, 2024

First try, without nailing it to any fixed CPU number: still the old problem, not RB.

Second try, nailing it to 4 cores: different hash, but still the same issue (dex diff looks like the one above). And it seems to be always com/best/deskclock/AlarmClockFragment – at least that is "constant" and "reproducible" (the error, not the APK).

I gave it a last try with JDK-21, but then the dex diff gets huge. 😢

@BlackyHawky
Copy link
Owner

BlackyHawky commented Dec 8, 2024

@IzzySoft
I am really disappointed because I followed all the recommendations. 😭

Could this be from Android Studio?
Maybe there is a difference between "Build" in the Android Studio menu and the "./gradlew assembleRelease" command? (Sorry if my question is stupid 😬)

Edit: After trying to sign the apk from the command line, the signed version has the same hash value as before. This means that building the apk from the Android Studio menu or using the command lines has no influence.

Is there nothing else I can do?

@IzzySoft
Copy link
Author

IzzySoft commented Dec 8, 2024

What about building on the commandline (./gradlew clean assembleRelease)? I have no idea about where Studio might leave and use "previous stuff" like artifacts etc as I never use it. It's not the signing that makes the difference here, but the building.

@BlackyHawky
Copy link
Owner

BlackyHawky commented Dec 9, 2024

@IzzySoft

What about building on the commandline (./gradlew clean assembleRelease)?

I get the same apk with the same hash value. I really don't understand what's wrong. 😭

@BlackyHawky
Copy link
Owner

Maybe it's the fact that I'm using Windows? See issue here on issuetracker.google.com.

@IzzySoft
Copy link
Author

IzzySoft commented Dec 9, 2024

I'm clueless now. Thought we had nailed it to be the "number of cores" issue. I can try another time if it's the "flaky build" issue as well (running that now)… Nope, each round the same hash.

Maybe it's the fact that I'm using Windows?

👀

Sounds like it. And that issue is from Fay, who confirmed it for multiple gradle versions. Well, sounds like it then, doesn't it? Any chance you could build on Linux (if you don't have a Linux machine/VM yourself, maybe using Github CI)?

EDIT: Nope, it's not that issue you've linked to. That we can work around (affected files are just plain text metadata inside META-INF/, or other plain text files in assets/ there – in your case it's the classes.dex which is binary).

@woj-tek
Copy link

woj-tek commented Dec 9, 2024

Uhm... I've been skimming over the issue and just tried on macOS:

$ git clone https://github.com/BlackyHawky/Clock.git
Cloning into 'Clock'...
remote: Enumerating objects: 88995, done.
remote: Counting objects: 100% (6031/6031), done.
remote: Compressing objects: 100% (2647/2647), done.
remote: Total 88995 (delta 3377), reused 5535 (delta 2988), pack-reused 82964 (from 1)
Receiving objects: 100% (88995/88995), 45.67 MiB | 15.24 MiB/s, done.
Resolving deltas: 100% (48420/48420), done.
[email protected] ~/dev/tmps $ cd Clock/
[email protected] ~/dev/tmps/Clock $ ./gradlew assembleRelease
-bash: ./gradlew: Permission denied
[email protected] ~/dev/tmps/Clock $ chmod +x gradlew
[email protected] ~/dev/tmps/Clock $ ./gradlew assembleRelease
Picked up JAVA_TOOL_OPTIONS:  -Duser.language=en   -Daether.dependencyCollector.impl=bf  -Dmaven.artifact.threads=5
Downloading https://services.gradle.org/distributions/gradle-8.11.1-bin.zip
.............10%.............20%.............30%.............40%.............50%.............60%.............70%.............80%.............90%.............100%
To honour the JVM settings for this build a single-use Daemon process will be forked. For more on this, please refer to https://docs.gradle.org/8.11.1/userguide/gradle_daemon.html#sec:disabling_the_daemon in the Gradle documentation.
Daemon will be stopped at the end of the build
Calculating task graph as no cached configuration is available for tasks: assembleRelease
Picked up JAVA_TOOL_OPTIONS:  -Duser.language=en   -Daether.dependencyCollector.impl=bf  -Dmaven.artifact.threads=5

Picked up JAVA_TOOL_OPTIONS:  -Duser.language=en   -Daether.dependencyCollector.impl=bf  -Dmaven.artifact.threads=5

Picked up JAVA_TOOL_OPTIONS:  -Duser.language=en   -Daether.dependencyCollector.impl=bf  -Dmaven.artifact.threads=5


> Task :app:compileReleaseJavaWithJavac
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
[Incubating] Problems report is available at: file:///Users/wojtek/dev/tmps/Clock/build/reports/problems/problems-report.html

Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.11.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD SUCCESSFUL in 59s
41 actionable tasks: 41 executed
Configuration cache entry stored.
[email protected] ~/dev/tmps/Clock $ sha256 ./app/build/outputs/apk/release/Clock_2.11-release-unsigned.apk
SHA256 (./app/build/outputs/apk/release/Clock_2.11-release-unsigned.apk) = 4d6c5c96d15fa1725ab5a714ce63500c085468330fc14dd827cbede23885172f

Though I'm not sure if changing permissions of gradlew had anything to do with it:

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   gradlew

@BlackyHawky if you are developing on windows - maybe trying WSL would help/yield different result/help move it forward? (windows has it's quirks…)

@BlackyHawky
Copy link
Owner

@IzzySoft
Yesterday, you said:

And it seems to be always com/best/deskclock/AlarmClockFragment

So I mainly modified this file and the linked xml file; then I did a "force-push" to overwrite the previous commit.

So can you do a new test please?

Below is the apk of the new version:
Clock_2.11-release.zip

Commit: 1d7f2f8408954cd1fab58f6ffce1ae4f98cbcc83

Hash: dbbde31d72c10d2d758ff28a4188cd5aa624e563eb597e503192df912030764c

@IzzySoft
Copy link
Author

IzzySoft commented Dec 9, 2024

Oof, that's not the only thing you changed, huh?

Configuration cache entry stored.FAILURE: Build failed with an exception.

* Where:
Build file '/build/repo/app/build.gradle' line: 20

* What went wrong:
A problem occurred evaluating project ':app'.
> /build/repo/keystore.properties (No such file or directory)

OK, working around that:

- echo -e "storeFile=/dev/null\nstorePassword=storePass\nkeyAlias=alias\nkeyPassword=keyPass" > keystore.properties
- sed -r '/signingConfigs.release/d' -i app/build.gradle

OK, build succeeds, but output file name has changed as well (so it failed to be copied out of the container for comparison), another round then…

Wow. I didn't limit CPUs, just to check – but it seems this did the trick:

    "upstream_signed_apk_sha256": "dbbde31d72c10d2d758ff28a4188cd5aa624e563eb597e503192df912030764c",
    "built_unsigned_apk_sha256": "27f444343099a14e5323f220455ca69a02f685dafa54cced3354b58dbf89b2ec",
    "signature_copied_apk_sha256": "dbbde31d72c10d2d758ff28a4188cd5aa624e563eb597e503192df912030764c"

That means this was RB 🥳 Congrats! What a ride, though 🙈

Going for the release then? 😃

@BlackyHawky
Copy link
Owner

@IzzySoft

Oof, that's not the only thing you changed, huh?

OK, build succeeds, but output file name has changed as well (so it failed to be copied out of the container for comparison), another round then…

Indeed, to be able to sign the apk using the command lines, I modified the "build.gradle" file by taking inspiration from this link. (Not sure if this is a correct method but it is very simple to implement and works correctly).
I also took the opportunity to change obsolete parameters, especially for the output file name.
Thank goodness you were able to adapt to these changes! 💪

Finally, I am very happy to see that my modification made in the "AlarmClockFragment.java" file was able to resolve this issue in the classes.dex. 🎉

Thank you for trusting me and and thank you for persevering with all these tests! 🎉💪👍👌

Going for the release then? 😃

If I'm not too tired after finishing my job I will do it later, otherwise tomorrow with my personal computer. 😉

@IzzySoft
Copy link
Author

IzzySoft commented Dec 9, 2024

Indeed, to be able to sign the apk using the command lines, I modified the "build.gradle" file

That enforces signing – so one couldn't build without a valid keystore (unless modifying the build.gradle as I just did). We know our way around that (as shown). Btw, I've seen different notations for that, so if you want to look into this (not a requirement, as our recipe "adjusts" it already), here's what I found and think have learned from it:

  • signingConfig signingConfigs.release/ signingConfigs["release"]: enforces signing
  • signingConfigs.getByName("release"): enforces signing
  • `signingConfigs.findByName("release"): doesn't enforce signing, but uses it when a valid signingConfig exists by that name

The latter would require making the signingConfig itself optional, or it would yell out when trying to access it. In my above snippet you see I still had to create a "fake signingConfig". The signingConfig can be wrapped in a "conditional". Pseudo-Code:

val propFile = rootProject.file("keystore.properties")
if (propFile.exists()) {
  // signingConfig here
}

Then, calling it via findByName should lead to a signed APK if the keystore.properties file exists – or an unsigned APK otherwise (well, or an error if the file is invalid – but that would happen currently as well).

Just to make sure: not saying you must change it, recipe is working now. Just sharing what I've learned on the topic, by having processed 367 successfully RB recipes in my builder alone 😜 So keep yours as it is if it works for you, and maybe stow away these details for… well, something in the future?

I also took the opportunity to change obsolete parameters, especially for the output file name.

Ah! That explains, thanks!

Thank goodness you were able to adapt to these changes! 💪

Hehe, 367+ recipes (not counting the 70+ in my backlog that did not (yet) succeed), you see a lot on the way. Guess I can tell stories at the camp fire about it 🙈

Finally, I am very happy to see that my modification made in the "AlarmClockFragment.java" file was able to resolve this issue in the classes.dex. 🎉

Same, same! That was great teamwork, wasn't it? Thanks a lot for your patience there!!! Oh, and I should not forget the… Ah, wait, we didn't yet add the recipe, still waiting for the next release. Oof, good! OK, later then…

Thank you for trusting me and and thank you for persevering with all these tests! 🎉💪👍👌

Gladly! It was quite a "bitter ride" – but so much sweeter the fruits of success! 🧺 🥝 🍌 🍒 Thanks again for sticking with me there!

If I'm not too tired after finishing my job I will do it later, otherwise tomorrow with my personal computer. 😉

Sure, take a break – well earned! Just give me a ping please once it's there. I'd then do a last test run, and then add the recipe to the builder and report back, so we can close this issue.

@BlackyHawky
Copy link
Owner

@IzzySoft

Just to make sure: not saying you must change it, recipe is working now. Just sharing what I've learned on the topic, by having processed 367 successfully RB recipes in my builder alone 😜 So keep yours as it is if it works for you, and maybe stow away these details for… well, something in the future?

If you don't mind, I wish we could fix this topic in the “build.gradle” file; indeed, even if it works for me (because I have the "keystore.properties" file), I am not a fan of this modification.

Also, it would require a very small modification that I should have thought of before (but I was pressed for time because of my job) ; I just have to write:

    if (keystorePropertiesFile.exists()) {
        keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
    }

    signingConfigs {
        release {
            if (keystorePropertiesFile.exists()) {
                storeFile = file("$rootDir/keystore.jks")
                storePassword = keystoreProperties["storePassword"]
                keyAlias = keystoreProperties["keyAlias"]
                keyPassword = keystoreProperties["keyPassword"]
            }
        }
    }

Or else I totally remove this modification about signingConfigs now that I know that building the apk from the Android Studio menu works just fine as before. 🤔

When you think about it the 2 main problems were the different number of cores in our processors and the "AlarmClockFragment.java" file.

Sure, take a break – well earned! Just give me a ping please once it's there. I'd then do a last test run, and then add the recipe to the builder and report back, so we can close this issue.

I think I'll do this tonight because I'm excited that we're finally validating our app as Reproducible Builds. 😉

@IzzySoft
Copy link
Author

IzzySoft commented Dec 9, 2024

Fine with me! If you make that modification, please don't forget to try

signingConfig signingConfigs.findByName("release")

(if that works there; I always get confused by what works in build.gradle and what might require build.gradle.kts – where this then would need to be signingConfig = signingConfigs.findByName("release")). And let me know your decision; I might need to add a marker in the YAML to manually adjust the recipe before the next run.

because I'm excited that we're finally validating our app as Reproducible Builds

Hehe – and so am I!

@BlackyHawky
Copy link
Owner

@IzzySoft
I'm not at all an expert when it comes to the "build.gradle" file but using signingConfig signingConfigs.findByName("release") doesn't seem to have any impact. So I included it in my change that allows the "build" without the "keystore.properties" file; in this case we obviously have an unsigned version.

We should note that the output name of the file agrees according to the build type:

  • build type "Release" with the "keystore.properties" file → Clock_2.11-release.apk
  • build type "Release" without the "keystore.properties" file → Clock_2.11-unsigned.apk
  • build type "Debug" → Clock_2.11-debug.apk

Like yesterday, I did a force-push to overwrite the old commit.

I hope that on your side, you won't have to adapt too much what you've already done and that it will (finally) be perfect. 😉

Below is the apk of the new version:
Clock_2.11-release.zip

Commit: 9613c72d902c181db905b0c9b88d48c8afff8ba7

Hash: 596f01dafca8e88b2a7220b55b86173172b1f2ee03fbd97cf9355b1e999a5074

@IzzySoft
Copy link
Author

    "upstream_signed_apk_sha256": "596f01dafca8e88b2a7220b55b86173172b1f2ee03fbd97cf9355b1e999a5074",
    "built_unsigned_apk_sha256": "41398898f743a1060796b81330936e105dd869e1f36e11c9184a710c15e9cf66",
    "signature_copied_apk_sha256": "596f01dafca8e88b2a7220b55b86173172b1f2ee03fbd97cf9355b1e999a5074"

with the simplest possible recipe:

build:
  - chmod +x gradlew # just to make sure, might not even be needed
  - ./gradlew assembleRelease

Done & RB.

@BlackyHawky
Copy link
Owner

Great news!!! 🎉🎉

I'll be releasing the new version shortly. I'll let you know when it's done.

@BlackyHawky
Copy link
Owner

@IzzySoft
It's done! 🎉🎉

@IzzySoft
Copy link
Author

BUILD SUCCESSFUL in 2m 26s
41 actionable tasks: 41 executed
Configuration cache entry stored.
+ mv app/build/outputs/apk/release/Clock_2.11-unsigned.apk /outputs/unsigned.apk

--- END BUILD LOG ---
Keeping '596f01dafca8e88b2a7220b55b86173172b1f2ee03fbd97cf9355b1e999a5074-com.best.deskclock-2.11-upstream.apk'...
Keeping '41398898f743a1060796b81330936e105dd869e1f36e11c9184a710c15e9cf66-com.best.deskclock-2.11-unsigned.apk'...
Reproducible: True
Tags built: 1.

It's done here, too – so 2.11 will arrive at the repo with the next sync around 7 pm UTC, and with the green shield up 🥳

🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants