diff options
Diffstat (limited to 'meta/lib/oe/reproducible.py')
-rw-r--r-- | meta/lib/oe/reproducible.py | 117 |
1 files changed, 104 insertions, 13 deletions
diff --git a/meta/lib/oe/reproducible.py b/meta/lib/oe/reproducible.py index 204b9bd734..0270024a83 100644 --- a/meta/lib/oe/reproducible.py +++ b/meta/lib/oe/reproducible.py | |||
@@ -1,10 +1,63 @@ | |||
1 | # | 1 | # |
2 | # Copyright OpenEmbedded Contributors | ||
3 | # | ||
2 | # SPDX-License-Identifier: GPL-2.0-only | 4 | # SPDX-License-Identifier: GPL-2.0-only |
3 | # | 5 | # |
4 | import os | 6 | import os |
5 | import subprocess | 7 | import subprocess |
6 | import bb | 8 | import bb |
7 | 9 | ||
10 | # For reproducible builds, this code sets the default SOURCE_DATE_EPOCH in each | ||
11 | # component's build environment. The format is number of seconds since the | ||
12 | # system epoch. | ||
13 | # | ||
14 | # Upstream components (generally) respect this environment variable, | ||
15 | # using it in place of the "current" date and time. | ||
16 | # See https://reproducible-builds.org/specs/source-date-epoch/ | ||
17 | # | ||
18 | # The default value of SOURCE_DATE_EPOCH comes from the function | ||
19 | # get_source_date_epoch_value which reads from the SDE_FILE, or if the file | ||
20 | # is not available will use the fallback of SOURCE_DATE_EPOCH_FALLBACK. | ||
21 | # | ||
22 | # The SDE_FILE is normally constructed from the function | ||
23 | # create_source_date_epoch_stamp which is typically added as a postfuncs to | ||
24 | # the do_unpack task. If a recipe does NOT have do_unpack, it should be added | ||
25 | # to a task that runs after the source is available and before the | ||
26 | # do_deploy_source_date_epoch task is executed. | ||
27 | # | ||
28 | # If a recipe wishes to override the default behavior it should set it's own | ||
29 | # SOURCE_DATE_EPOCH or override the do_deploy_source_date_epoch_stamp task | ||
30 | # with recipe-specific functionality to write the appropriate | ||
31 | # SOURCE_DATE_EPOCH into the SDE_FILE. | ||
32 | # | ||
33 | # SOURCE_DATE_EPOCH is intended to be a reproducible value. This value should | ||
34 | # be reproducible for anyone who builds the same revision from the same | ||
35 | # sources. | ||
36 | # | ||
37 | # There are 4 ways the create_source_date_epoch_stamp function determines what | ||
38 | # becomes SOURCE_DATE_EPOCH: | ||
39 | # | ||
40 | # 1. Use the value from __source_date_epoch.txt file if this file exists. | ||
41 | # This file was most likely created in the previous build by one of the | ||
42 | # following methods 2,3,4. | ||
43 | # Alternatively, it can be provided by a recipe via SRC_URI. | ||
44 | # | ||
45 | # If the file does not exist: | ||
46 | # | ||
47 | # 2. If there is a git checkout, use the last git commit timestamp. | ||
48 | # Git does not preserve file timestamps on checkout. | ||
49 | # | ||
50 | # 3. Use the mtime of "known" files such as NEWS, CHANGELOG, ... | ||
51 | # This works for well-kept repositories distributed via tarball. | ||
52 | # | ||
53 | # 4. Use the modification time of the youngest file in the source tree, if | ||
54 | # there is one. | ||
55 | # This will be the newest file from the distribution tarball, if any. | ||
56 | # | ||
57 | # 5. Fall back to a fixed timestamp (SOURCE_DATE_EPOCH_FALLBACK). | ||
58 | # | ||
59 | # Once the value is determined, it is stored in the recipe's SDE_FILE. | ||
60 | |||
8 | def get_source_date_epoch_from_known_files(d, sourcedir): | 61 | def get_source_date_epoch_from_known_files(d, sourcedir): |
9 | source_date_epoch = None | 62 | source_date_epoch = None |
10 | newest_file = None | 63 | newest_file = None |
@@ -22,10 +75,11 @@ def get_source_date_epoch_from_known_files(d, sourcedir): | |||
22 | return source_date_epoch | 75 | return source_date_epoch |
23 | 76 | ||
24 | def find_git_folder(d, sourcedir): | 77 | def find_git_folder(d, sourcedir): |
25 | # First guess: WORKDIR/git | 78 | # First guess: UNPACKDIR/BB_GIT_DEFAULT_DESTSUFFIX |
26 | # This is the default git fetcher unpack path | 79 | # This is the default git fetcher unpack path |
27 | workdir = d.getVar('WORKDIR') | 80 | unpackdir = d.getVar('UNPACKDIR') |
28 | gitpath = os.path.join(workdir, "git/.git") | 81 | default_destsuffix = d.getVar('BB_GIT_DEFAULT_DESTSUFFIX') |
82 | gitpath = os.path.join(unpackdir, default_destsuffix, ".git") | ||
29 | if os.path.isdir(gitpath): | 83 | if os.path.isdir(gitpath): |
30 | return gitpath | 84 | return gitpath |
31 | 85 | ||
@@ -35,15 +89,16 @@ def find_git_folder(d, sourcedir): | |||
35 | return gitpath | 89 | return gitpath |
36 | 90 | ||
37 | # Perhaps there was a subpath or destsuffix specified. | 91 | # Perhaps there was a subpath or destsuffix specified. |
38 | # Go looking in the WORKDIR | 92 | # Go looking in the UNPACKDIR |
39 | exclude = set(["build", "image", "license-destdir", "patches", "pseudo", | 93 | for root, dirs, files in os.walk(unpackdir, topdown=True): |
40 | "recipe-sysroot", "recipe-sysroot-native", "sysroot-destdir", "temp"]) | ||
41 | for root, dirs, files in os.walk(workdir, topdown=True): | ||
42 | dirs[:] = [d for d in dirs if d not in exclude] | ||
43 | if '.git' in dirs: | 94 | if '.git' in dirs: |
44 | return root | 95 | return os.path.join(root, ".git") |
45 | 96 | ||
46 | bb.warn("Failed to find a git repository in WORKDIR: %s" % workdir) | 97 | for root, dirs, files in os.walk(sourcedir, topdown=True): |
98 | if '.git' in dirs: | ||
99 | return os.path.join(root, ".git") | ||
100 | |||
101 | bb.warn("Failed to find a git repository in UNPACKDIR: %s" % unpackdir) | ||
47 | return None | 102 | return None |
48 | 103 | ||
49 | def get_source_date_epoch_from_git(d, sourcedir): | 104 | def get_source_date_epoch_from_git(d, sourcedir): |
@@ -62,11 +117,12 @@ def get_source_date_epoch_from_git(d, sourcedir): | |||
62 | return None | 117 | return None |
63 | 118 | ||
64 | bb.debug(1, "git repository: %s" % gitpath) | 119 | bb.debug(1, "git repository: %s" % gitpath) |
65 | p = subprocess.run(['git', '--git-dir', gitpath, 'log', '-1', '--pretty=%ct'], check=True, stdout=subprocess.PIPE) | 120 | p = subprocess.run(['git', '-c', 'log.showSignature=false', '--git-dir', gitpath, 'log', '-1', '--pretty=%ct'], |
121 | check=True, stdout=subprocess.PIPE) | ||
66 | return int(p.stdout.decode('utf-8')) | 122 | return int(p.stdout.decode('utf-8')) |
67 | 123 | ||
68 | def get_source_date_epoch_from_youngest_file(d, sourcedir): | 124 | def get_source_date_epoch_from_youngest_file(d, sourcedir): |
69 | if sourcedir == d.getVar('WORKDIR'): | 125 | if sourcedir == d.getVar('UNPACKDIR'): |
70 | # These sources are almost certainly not from a tarball | 126 | # These sources are almost certainly not from a tarball |
71 | return None | 127 | return None |
72 | 128 | ||
@@ -77,6 +133,9 @@ def get_source_date_epoch_from_youngest_file(d, sourcedir): | |||
77 | files = [f for f in files if not f[0] == '.'] | 133 | files = [f for f in files if not f[0] == '.'] |
78 | 134 | ||
79 | for fname in files: | 135 | for fname in files: |
136 | if fname == "singletask.lock": | ||
137 | # Ignore externalsrc/devtool lockfile [YOCTO #14921] | ||
138 | continue | ||
80 | filename = os.path.join(root, fname) | 139 | filename = os.path.join(root, fname) |
81 | try: | 140 | try: |
82 | mtime = int(os.lstat(filename).st_mtime) | 141 | mtime = int(os.lstat(filename).st_mtime) |
@@ -101,8 +160,40 @@ def fixed_source_date_epoch(d): | |||
101 | def get_source_date_epoch(d, sourcedir): | 160 | def get_source_date_epoch(d, sourcedir): |
102 | return ( | 161 | return ( |
103 | get_source_date_epoch_from_git(d, sourcedir) or | 162 | get_source_date_epoch_from_git(d, sourcedir) or |
104 | get_source_date_epoch_from_known_files(d, sourcedir) or | ||
105 | get_source_date_epoch_from_youngest_file(d, sourcedir) or | 163 | get_source_date_epoch_from_youngest_file(d, sourcedir) or |
106 | fixed_source_date_epoch(d) # Last resort | 164 | fixed_source_date_epoch(d) # Last resort |
107 | ) | 165 | ) |
108 | 166 | ||
167 | def epochfile_read(epochfile, d): | ||
168 | cached, efile = d.getVar('__CACHED_SOURCE_DATE_EPOCH') or (None, None) | ||
169 | if cached and efile == epochfile: | ||
170 | return cached | ||
171 | |||
172 | if cached and epochfile != efile: | ||
173 | bb.debug(1, "Epoch file changed from %s to %s" % (efile, epochfile)) | ||
174 | |||
175 | source_date_epoch = int(d.getVar('SOURCE_DATE_EPOCH_FALLBACK')) | ||
176 | try: | ||
177 | with open(epochfile, 'r') as f: | ||
178 | s = f.read() | ||
179 | try: | ||
180 | source_date_epoch = int(s) | ||
181 | except ValueError: | ||
182 | bb.warn("SOURCE_DATE_EPOCH value '%s' is invalid. Reverting to SOURCE_DATE_EPOCH_FALLBACK" % s) | ||
183 | source_date_epoch = int(d.getVar('SOURCE_DATE_EPOCH_FALLBACK')) | ||
184 | bb.debug(1, "SOURCE_DATE_EPOCH: %d" % source_date_epoch) | ||
185 | except FileNotFoundError: | ||
186 | bb.debug(1, "Cannot find %s. SOURCE_DATE_EPOCH will default to %d" % (epochfile, source_date_epoch)) | ||
187 | |||
188 | d.setVar('__CACHED_SOURCE_DATE_EPOCH', (str(source_date_epoch), epochfile)) | ||
189 | return str(source_date_epoch) | ||
190 | |||
191 | def epochfile_write(source_date_epoch, epochfile, d): | ||
192 | |||
193 | bb.debug(1, "SOURCE_DATE_EPOCH: %d" % source_date_epoch) | ||
194 | bb.utils.mkdirhier(os.path.dirname(epochfile)) | ||
195 | |||
196 | tmp_file = "%s.new" % epochfile | ||
197 | with open(tmp_file, 'w') as f: | ||
198 | f.write(str(source_date_epoch)) | ||
199 | os.rename(tmp_file, epochfile) | ||