@@ -102,6 +102,167 @@ jobs:
102102 uses : actions/deploy-pages@v4
103103 id : deployment
104104
105+ # Hubris builds have to be reproducible, and we want to test that in CI. This job does a build of
106+ # an arbitrary board (specifically cosmo-b, but it can be changed to any board) with the standard
107+ # Ubuntu image and no interference, as the baseline to compire to.
108+ reproducible-a :
109+ name : Reproducibility (A)
110+ runs-on : ubuntu-latest
111+ permissions :
112+ contents : read
113+ steps :
114+ - name : Checkout the source code
115+ uses : actions/checkout@v6
116+
117+ # We check explicitly to ensure the other job has a different one.
118+ - name : Check that GCC is the system C toolchain
119+ run : cc --version | grep -q "Free Software Foundation"
120+
121+ - name : Build a Hubris board
122+ run : |
123+ umask 0007 # We set the umask explicitly here to ensure the other job has a different one.
124+ cargo xtask dist app/cosmo/rev-b.toml
125+
126+ - name : Upload the artifact to be later checked
127+ uses : actions/upload-artifact@v6
128+ with :
129+ name : reproducible-a
130+ path : target/cosmo-b/dist/default/build-cosmo-b-image-default.zip
131+ if-no-files-found : error
132+
133+ # Hubris builds have to be reproducible, and we want to test that in CI. This job does a build of
134+ # an arbitrary board (specifically cosmo-b, but it can be changed to any board) trying to change
135+ # the build environment as further as possible from reproducible-a. Each variability we introduce
136+ # and its reasoning is documented in comments below.
137+ #
138+ # More information on common variations are available on the reproducible-builds website:
139+ # https://reproducible-builds.org/docs/env-variations/
140+ reproducible-b :
141+ name : Reproducibility (B)
142+ runs-on : ubuntu-latest
143+ permissions :
144+ contents : read
145+ env :
146+ CUSTOM_ROOT : /very/long/path/we/are/doing/the/build/in/to/check/for/issues/with/long/paths/or/different/paths
147+ steps :
148+ - name : Install Ubuntu dependencies
149+ run : |
150+ sudo apt-get update
151+ sudo apt-get install -y disorderfs clang
152+ sudo apt-get remove -y gcc
153+ sudo apt-get autoremove -y
154+
155+ # In the Ubuntu dependencies installation step above we switched from GCC to Clang as the
156+ # system C toolchain and linker. We are not using the system linker in the build process
157+ # (Hubris uses the LLD copy bundled with Rust), so switching the system toolchain will catch
158+ # us accidentally relying on it (and breaking reproducibility depending on which toolchain is
159+ # installed on the system attempting to reproduce).
160+ - name : Check that clang is the system C toolchain
161+ run : |
162+ ! command -v gcc >/dev/null
163+ cc --version | grep -q clang
164+
165+ # GitHub Actions is really annoying and doesn't let us clone repositories with the checkout
166+ # action outside of $GITHUB_WORKSPACE, erroring out if we dare to do so. We still want to use
167+ # the action though, as it will gracefully handle authentication if someone creates a private
168+ # clone of Hubris. We thus clone the repository in the default path, and then in a later step
169+ # clone this checkout into the custom build directory.
170+ - name : Checkout the source code in the standard GitHub Actions directory
171+ uses : actions/checkout@v6
172+
173+ # We run the Hubris build in a different directory, to ensure that paths are not hardcoded. We
174+ # also use a very long path, as a reproducibility issue Emily found in the wild in the past
175+ # was a rust-lang/rust test failing when built in a path that was too long.
176+ #
177+ # We also use disorderfs to randomize the ordering of listing directories, to catch code
178+ # assuming directory entries are always returned in the same order.
179+ - name : Prepare a custom build root directory with disorderfs
180+ run : |
181+ sudo mkdir -p $CUSTOM_ROOT
182+ sudo disorderfs --multi-user=yes --shuffle-dirents=yes $(pwd) $CUSTOM_ROOT
183+
184+ # The current time might be included in the built artifacts. To ensure reproducibility, move
185+ # the time forward by a day and a few hours. This should be enough to expose differences in
186+ # the build without messing with TLS certificate expiration.
187+ #
188+ # Note that this causes very funny behavior in GitHub Action's workflow UI, as apparently step
189+ # duration estimates are based on time reported by the runner???
190+ - name : Move forward in time to ensure a different build date
191+ run : |
192+ sudo timedatectl set-ntp false
193+ sudo timedatectl set-time "$(date -d '1 day ago 11 hours ago' "+%Y-%m-%d %H:%M:%S")"
194+ date
195+
196+ - name : Build a Hubris board
197+ run : |
198+ # Permissions of files created during the build process might leak into the artifacts.
199+ # Changing the umask will let us test with different permissions than archives created in
200+ # the reproducible-a job.
201+ umask 0077
202+
203+ cd $CUSTOM_ROOT
204+ cargo xtask dist app/cosmo/rev-b.toml
205+
206+ - name : Move back to the right time
207+ run : sudo timedatectl set-ntp true
208+
209+ - name : Upload the artifact to be later checked
210+ uses : actions/upload-artifact@v6
211+ with :
212+ name : reproducible-b
213+ path : ${{ env.CUSTOM_ROOT }}/target/cosmo-b/dist/default/build-cosmo-b-image-default.zip
214+ if-no-files-found : error
215+
216+ reproducible-check :
217+ name : Reproducibility check
218+ runs-on : ubuntu-slim
219+ needs :
220+ - reproducible-a
221+ - reproducible-b
222+ permissions : {}
223+ steps :
224+ - name : Install uv (Python package manager)
225+ uses : astral-sh/setup-uv@v7
226+ with :
227+ enable-cache : false
228+ ignore-empty-workdir : true
229+
230+ - name : Download reproducible artifacts
231+ uses : actions/download-artifact@v7
232+ with :
233+ pattern : reproducible-*
234+
235+ # Diffoscope is a tool built by the reproducible-builds people to do a rich format-aware diff
236+ # of two files. For example, it understands both zip archives and ELF objects, so it can point
237+ # out the member of archive or the ELF section containing the difference.
238+ #
239+ # We are pulling diffoscope from PyPI transparently through `uvx`, instead of installing it
240+ # from the Ubuntu archives with `apt-get`. We're doing this because due to (mostly sensible)
241+ # packaging choice installing the Ubuntu package takes 6+ minutes, compared to the sub-second
242+ # installation time `uvx` provides. The fact we get a newer version doesn't hurt either.
243+ #
244+ # If you are curious, the reason why the Ubuntu package takes so long to install is because
245+ # diffoscope can produce better diffs the more CLI tools are installed, and the Ubuntu package
246+ # depends on all of those tools. We don't really care about all of them, and the barebones
247+ # version installed through PyPI is enough for us.
248+ - name : Compare the two reproducible artifacts
249+ run : uvx diffoscope --html report.html reproducible-a/build-cosmo-b-image-default.zip reproducible-b/build-cosmo-b-image-default.zip
250+
251+ - name : Upload the diffoscope report
252+ if : failure() # Only upload the report if the previous step failed.
253+ id : diffoscope-report
254+ uses : actions/upload-artifact@v6
255+ with :
256+ name : reproducible-diffoscope-report
257+ path : report.html
258+ if-no-files-found : error
259+
260+ - name : Add a job summary to point folks to diffoscope
261+ if : failure()
262+ run : echo "Non-reproducibility was detected by CI. [Download the diffoscope report]($REPORT_URL) to learn more" >> $GITHUB_STEP_SUMMARY
263+ env :
264+ REPORT_URL : ${{ steps.diffoscope-report.outputs.artifact-url }}
265+
105266 finish :
106267 name : CI finished
107268 runs-on : ubuntu-slim
@@ -112,6 +273,7 @@ jobs:
112273 - format
113274 - docs-build
114275 - docs-deploy
276+ - reproducible-check
115277 if : " ${{ !cancelled() }}"
116278 steps :
117279 - name : Calculate the correct exit status
0 commit comments