]> Git Repo - buildroot-mgba.git/blob - utils/genrandconfig
utils/genrandconfig: only do reproducible builds with diffoscope
[buildroot-mgba.git] / utils / genrandconfig
1 #!/usr/bin/env python
2
3 # Copyright (C) 2014 by Thomas Petazzoni <[email protected]>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
19 # This script generates a random configuration for testing Buildroot.
20
21 from __future__ import print_function
22
23 import contextlib
24 import csv
25 import os
26 from random import randint
27 import subprocess
28 import sys
29 from distutils.version import StrictVersion
30 import platform
31
32 if sys.hexversion >= 0x3000000:
33     import urllib.request as _urllib
34 else:
35     import urllib2 as _urllib
36
37
38 def urlopen_closing(uri):
39     return contextlib.closing(_urllib.urlopen(uri))
40
41
42 if sys.hexversion >= 0x3000000:
43     def decode_byte_list(bl):
44         return [b.decode() for b in bl]
45 else:
46     def decode_byte_list(e):
47         return e
48
49
50 class SystemInfo:
51     DEFAULT_NEEDED_PROGS = ["make", "git", "gcc", "timeout"]
52     DEFAULT_OPTIONAL_PROGS = ["bzr", "java", "javac", "jar", "diffoscope"]
53
54     def __init__(self):
55         self.needed_progs = list(self.__class__.DEFAULT_NEEDED_PROGS)
56         self.optional_progs = list(self.__class__.DEFAULT_OPTIONAL_PROGS)
57         self.progs = {}
58
59     def find_prog(self, name, flags=os.X_OK, env=os.environ):
60         if not name or name[0] == os.sep:
61             raise ValueError(name)
62
63         prog_path = env.get("PATH", None)
64         # for windows compatibility, we'd need to take PATHEXT into account
65
66         if prog_path:
67             for prog_dir in filter(None, prog_path.split(os.pathsep)):
68                 # os.join() not necessary: non-empty prog_dir
69                 # and name[0] != os.sep
70                 prog = prog_dir + os.sep + name
71                 if os.access(prog, flags):
72                     return prog
73         # --
74         return None
75
76     def has(self, prog):
77         """Checks whether a program is available.
78         Lazily evaluates missing entries.
79
80         Returns: None if prog not found, else path to the program [evaluates
81         to True]
82         """
83         try:
84             return self.progs[prog]
85         except KeyError:
86             pass
87
88         have_it = self.find_prog(prog)
89         # java[c] needs special care
90         if have_it and prog in ('java', 'javac'):
91             with open(os.devnull, "w") as devnull:
92                 if subprocess.call("%s -version | grep gcj" % prog,
93                                    shell=True,
94                                    stdout=devnull, stderr=devnull) != 1:
95                     have_it = False
96         # --
97         self.progs[prog] = have_it
98         return have_it
99
100     def check_requirements(self):
101         """Checks program dependencies.
102
103         Returns: True if all mandatory programs are present, else False.
104         """
105         do_check_has_prog = self.has
106
107         missing_requirements = False
108         for prog in self.needed_progs:
109             if not do_check_has_prog(prog):
110                 print("ERROR: your system lacks the '%s' program" % prog)
111                 missing_requirements = True
112
113         # check optional programs here,
114         # else they'd get checked by each worker instance
115         for prog in self.optional_progs:
116             do_check_has_prog(prog)
117
118         return not missing_requirements
119
120
121 def get_toolchain_configs(toolchains_csv, buildrootdir):
122     """Fetch and return the possible toolchain configurations
123
124     This function returns an array of toolchain configurations. Each
125     toolchain configuration is itself an array of lines of the defconfig.
126     """
127
128     with open(toolchains_csv) as r:
129         # filter empty lines and comments
130         lines = [t for t in r.readlines() if len(t.strip()) > 0 and t[0] != '#']
131         toolchains = decode_byte_list(lines)
132     configs = []
133
134     (_, _, _, _, hostarch) = os.uname()
135     # ~2015 distros report x86 when on a 32bit install
136     if hostarch == 'i686' or hostarch == 'i386' or hostarch == 'x86':
137         hostarch = 'x86'
138
139     for row in csv.reader(toolchains):
140         config = {}
141         configfile = row[0]
142         config_hostarch = row[1]
143         keep = False
144
145         # Keep all toolchain configs that work regardless of the host
146         # architecture
147         if config_hostarch == "any":
148             keep = True
149
150         # Keep all toolchain configs that can work on the current host
151         # architecture
152         if hostarch == config_hostarch:
153             keep = True
154
155         # Assume that x86 32 bits toolchains work on x86_64 build
156         # machines
157         if hostarch == 'x86_64' and config_hostarch == "x86":
158             keep = True
159
160         if not keep:
161             continue
162
163         if not os.path.isabs(configfile):
164             configfile = os.path.join(buildrootdir, configfile)
165
166         with open(configfile) as r:
167             config = r.readlines()
168         configs.append(config)
169     return configs
170
171
172 def is_toolchain_usable(configfile, config):
173     """Check if the toolchain is actually usable."""
174
175     with open(configfile) as configf:
176         configlines = configf.readlines()
177
178     # Check that the toolchain configuration is still present
179     for toolchainline in config:
180         if toolchainline not in configlines:
181             print("WARN: toolchain can't be used", file=sys.stderr)
182             print("      Missing: %s" % toolchainline.strip(), file=sys.stderr)
183             return False
184
185     # The latest Linaro toolchains on x86-64 hosts requires glibc
186     # 2.14+ on the host.
187     if platform.machine() == 'x86_64':
188         if 'BR2_TOOLCHAIN_EXTERNAL_LINARO_ARM=y\n' in configlines or \
189            'BR2_TOOLCHAIN_EXTERNAL_LINARO_AARCH64=y\n' in configlines or \
190            'BR2_TOOLCHAIN_EXTERNAL_LINARO_AARCH64_BE=y\n' in configlines or \
191            'BR2_TOOLCHAIN_EXTERNAL_LINARO_ARMEB=y\n' in configlines:
192             ldd_version_output = subprocess.check_output(['ldd', '--version'])
193             glibc_version = ldd_version_output.splitlines()[0].split()[-1]
194             if StrictVersion('2.14') > StrictVersion(glibc_version):
195                 print("WARN: ignoring the Linaro ARM toolchains because too old host glibc", file=sys.stderr)
196                 return False
197
198     return True
199
200
201 def fixup_config(sysinfo, configfile):
202     """Finalize the configuration and reject any problematic combinations
203
204     This function returns 'True' when the configuration has been
205     accepted, and 'False' when the configuration has not been accepted because
206     it is known to fail (in which case another random configuration will be
207     generated).
208     """
209
210     with open(configfile) as configf:
211         configlines = configf.readlines()
212
213     BR2_TOOLCHAIN_EXTERNAL_URL = 'BR2_TOOLCHAIN_EXTERNAL_URL="http://autobuild.buildroot.org/toolchains/tarballs/'
214
215     if "BR2_NEEDS_HOST_JAVA=y\n" in configlines and not sysinfo.has("java"):
216         return False
217     if "BR2_NEEDS_HOST_JAVAC=y\n" in configlines and not sysinfo.has("javac"):
218         return False
219     if "BR2_NEEDS_HOST_JAR=y\n" in configlines and not sysinfo.has("jar"):
220         return False
221     # python-nfc needs bzr
222     if 'BR2_PACKAGE_PYTHON_NFC=y\n' in configlines and not sysinfo.has("bzr"):
223         return False
224     # The ctng toolchain is affected by PR58854
225     if 'BR2_PACKAGE_LTTNG_TOOLS=y\n' in configlines and \
226        BR2_TOOLCHAIN_EXTERNAL_URL + 'armv5-ctng-linux-gnueabi.tar.xz"\n' in configlines:
227         return False
228     # The ctng toolchain tigger an assembler error with guile package when compiled with -Os (same issue as for CS ARM 2014.05-29)
229     if 'BR2_PACKAGE_GUILE=y\n' in configlines and \
230        'BR2_OPTIMIZE_S=y\n' in configlines and \
231        BR2_TOOLCHAIN_EXTERNAL_URL + 'armv5-ctng-linux-gnueabi.tar.xz"\n' in configlines:
232         return False
233     # The ctng toolchain is affected by PR58854
234     if 'BR2_PACKAGE_LTTNG_TOOLS=y\n' in configlines and \
235        BR2_TOOLCHAIN_EXTERNAL_URL + 'armv6-ctng-linux-uclibcgnueabi.tar.xz"\n' in configlines:
236         return False
237     # The ctng toolchain is affected by PR58854
238     if 'BR2_PACKAGE_LTTNG_TOOLS=y\n' in configlines and \
239        BR2_TOOLCHAIN_EXTERNAL_URL + 'armv7-ctng-linux-gnueabihf.tar.xz"\n' in configlines:
240         return False
241     # The ctng toolchain is affected by PR60155
242     if 'BR2_PACKAGE_SDL=y\n' in configlines and \
243        BR2_TOOLCHAIN_EXTERNAL_URL + 'powerpc-ctng-linux-uclibc.tar.xz"\n' in configlines:
244         return False
245     # The ctng toolchain is affected by PR60155
246     if 'BR2_PACKAGE_LIBMPEG2=y\n' in configlines and \
247        BR2_TOOLCHAIN_EXTERNAL_URL + 'powerpc-ctng-linux-uclibc.tar.xz"\n' in configlines:
248         return False
249     # This MIPS toolchain uses eglibc-2.18 which lacks SYS_getdents64
250     if 'BR2_PACKAGE_STRONGSWAN=y\n' in configlines and \
251        BR2_TOOLCHAIN_EXTERNAL_URL + 'mips64el-ctng_n64-linux-gnu.tar.xz"\n' in configlines:
252         return False
253     # This MIPS toolchain uses eglibc-2.18 which lacks SYS_getdents64
254     if 'BR2_PACKAGE_PYTHON3=y\n' in configlines and \
255        BR2_TOOLCHAIN_EXTERNAL_URL + 'mips64el-ctng_n64-linux-gnu.tar.xz"\n' in configlines:
256         return False
257     # libffi not available on sh2a and ARMv7-M, but propagating libffi
258     # arch dependencies in Buildroot is really too much work, so we
259     # handle this here.
260     if 'BR2_sh2a=y\n' in configlines and \
261        'BR2_PACKAGE_LIBFFI=y\n' in configlines:
262         return False
263     if 'BR2_ARM_CPU_ARMV7M=y\n' in configlines and \
264        'BR2_PACKAGE_LIBFFI=y\n' in configlines:
265         return False
266     if 'BR2_nds32=y\n' in configlines and \
267        'BR2_PACKAGE_LIBFFI=y\n' in configlines:
268         return False
269     if 'BR2_PACKAGE_SUNXI_BOARDS=y\n' in configlines:
270         configlines.remove('BR2_PACKAGE_SUNXI_BOARDS_FEX_FILE=""\n')
271         configlines.append('BR2_PACKAGE_SUNXI_BOARDS_FEX_FILE="a10/hackberry.fex"\n')
272     # This MIPS uClibc toolchain fails to build the gdb package
273     if 'BR2_PACKAGE_GDB=y\n' in configlines and \
274        BR2_TOOLCHAIN_EXTERNAL_URL + 'mipsel-ctng-linux-uclibc.tar.xz"\n' in configlines:
275         return False
276     # This MIPS uClibc toolchain fails to build the rt-tests package
277     if 'BR2_PACKAGE_RT_TESTS=y\n' in configlines and \
278        BR2_TOOLCHAIN_EXTERNAL_URL + 'mipsel-ctng-linux-uclibc.tar.xz"\n' in configlines:
279         return False
280     # This MIPS uClibc toolchain fails to build the civetweb package
281     if 'BR2_PACKAGE_CIVETWEB=y\n' in configlines and \
282        BR2_TOOLCHAIN_EXTERNAL_URL + 'mipsel-ctng-linux-uclibc.tar.xz"\n' in configlines:
283         return False
284     # This MIPS ctng toolchain fails to build the python3 package
285     if 'BR2_PACKAGE_PYTHON3=y\n' in configlines and \
286        BR2_TOOLCHAIN_EXTERNAL_URL + 'mips64el-ctng_n64-linux-gnu.tar.xz"\n' in configlines:
287         return False
288     # This MIPS uClibc toolchain fails to build the strace package
289     if 'BR2_PACKAGE_STRACE=y\n' in configlines and \
290        BR2_TOOLCHAIN_EXTERNAL_URL + 'mipsel-ctng-linux-uclibc.tar.xz"\n' in configlines:
291         return False
292     # This MIPS uClibc toolchain fails to build the cdrkit package
293     if 'BR2_PACKAGE_CDRKIT=y\n' in configlines and \
294        'BR2_STATIC_LIBS=y\n' in configlines and \
295        BR2_TOOLCHAIN_EXTERNAL_URL + 'mipsel-ctng-linux-uclibc.tar.xz"\n' in configlines:
296         return False
297     # uClibc vfork static linking issue
298     if 'BR2_PACKAGE_ALSA_LIB=y\n' in configlines and \
299        'BR2_STATIC_LIBS=y\n' in configlines and \
300        BR2_TOOLCHAIN_EXTERNAL_URL + 'i486-ctng-linux-uclibc.tar.xz"\n' in configlines:
301         return False
302     # This MIPS uClibc toolchain fails to build the weston package
303     if 'BR2_PACKAGE_WESTON=y\n' in configlines and \
304        BR2_TOOLCHAIN_EXTERNAL_URL + 'mipsel-ctng-linux-uclibc.tar.xz"\n' in configlines:
305         return False
306     # The cs nios2 2017.02 toolchain is affected by binutils PR19405
307     if 'BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_NIOSII=y\n' in configlines and \
308        'BR2_PACKAGE_BOOST=y\n' in configlines:
309         return False
310     # The cs nios2 2017.02 toolchain is affected by binutils PR19405
311     if 'BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_NIOSII=y\n' in configlines and \
312        'BR2_PACKAGE_QT5BASE_GUI=y\n' in configlines:
313         return False
314     # The cs nios2 2017.02 toolchain is affected by binutils PR19405
315     if 'BR2_TOOLCHAIN_EXTERNAL_CODESOURCERY_NIOSII=y\n' in configlines and \
316        'BR2_PACKAGE_FLANN=y\n' in configlines:
317         return False
318
319     with open(configfile, "w+") as configf:
320         configf.writelines(configlines)
321
322     return True
323
324
325 def gen_config(args):
326     """Generate a new random configuration
327
328     This function generates the configuration, by choosing a random
329     toolchain configuration and then generating a random selection of
330     packages.
331     """
332
333     sysinfo = SystemInfo()
334
335     # Select a random toolchain configuration
336     configs = get_toolchain_configs(args.toolchains_csv, args.buildrootdir)
337
338     i = randint(0, len(configs) - 1)
339     toolchainconfig = configs[i]
340
341     configlines = list(toolchainconfig)
342
343     # Combine with the minimal configuration
344     minimalconfigfile = os.path.join(args.buildrootdir,
345                                      'support/config-fragments/minimal.config')
346     with open(minimalconfigfile) as minimalf:
347         configlines += minimalf.readlines()
348
349     # Allow hosts with old certificates to download over https
350     configlines.append("BR2_WGET=\"wget --passive-ftp -nd -t 3 --no-check-certificate\"\n")
351
352     # Amend the configuration with a few things.
353     if randint(0, 20) == 0:
354         configlines.append("BR2_ENABLE_DEBUG=y\n")
355     if randint(0, 1) == 0:
356         configlines.append("BR2_INIT_BUSYBOX=y\n")
357     elif randint(0, 15) == 0:
358         configlines.append("BR2_INIT_SYSTEMD=y\n")
359     elif randint(0, 10) == 0:
360         configlines.append("BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y\n")
361     if randint(0, 20) == 0:
362         configlines.append("BR2_STATIC_LIBS=y\n")
363     if randint(0, 20) == 0:
364         configlines.append("BR2_PACKAGE_PYTHON_PY_ONLY=y\n")
365     if randint(0, 5) == 0:
366         configlines.append("BR2_OPTIMIZE_2=y\n")
367     if randint(0, 4) == 0:
368         configlines.append("BR2_SYSTEM_ENABLE_NLS=y\n")
369
370     # Randomly enable BR2_REPRODUCIBLE 10% of times
371     # also enable tar filesystem images for testing
372     if sysinfo.has("diffoscope") and randint(0, 10) == 0:
373         configlines.append("BR2_REPRODUCIBLE=y\n")
374         configlines.append("BR2_TARGET_ROOTFS_TAR=y\n")
375
376     # Write out the configuration file
377     if not os.path.exists(args.outputdir):
378         os.makedirs(args.outputdir)
379     if args.outputdir == os.path.abspath(os.path.join(args.buildrootdir, "output")):
380         configfile = os.path.join(args.buildrootdir, ".config")
381     else:
382         configfile = os.path.join(args.outputdir, ".config")
383     with open(configfile, "w+") as configf:
384         configf.writelines(configlines)
385
386     subprocess.check_call(["make", "O=%s" % args.outputdir, "-C", args.buildrootdir,
387                            "olddefconfig"])
388
389     if not is_toolchain_usable(configfile, toolchainconfig):
390         return 2
391
392     # Now, generate the random selection of packages, and fixup
393     # things if needed.
394     # Safe-guard, in case we can not quickly come to a valid
395     # configuration: allow at most 100 (arbitrary) iterations.
396     bounded_loop = 100
397     while True:
398         if bounded_loop == 0:
399             print("ERROR: cannot generate random configuration after 100 iterations",
400                   file=sys.stderr)
401             return 1
402         bounded_loop -= 1
403         subprocess.check_call(["make", "O=%s" % args.outputdir, "-C", args.buildrootdir,
404                                "KCONFIG_PROBABILITY=%d" % randint(1, 30),
405                                "randpackageconfig"])
406
407         if fixup_config(sysinfo, configfile):
408             break
409
410     subprocess.check_call(["make", "O=%s" % args.outputdir, "-C", args.buildrootdir,
411                            "olddefconfig"])
412
413     subprocess.check_call(["make", "O=%s" % args.outputdir, "-C", args.buildrootdir,
414                            "savedefconfig"])
415
416     return subprocess.call(["make", "O=%s" % args.outputdir, "-C", args.buildrootdir,
417                             "dependencies"])
418
419
420 if __name__ == '__main__':
421     import argparse
422     parser = argparse.ArgumentParser(description="Generate a random configuration")
423     parser.add_argument("--outputdir", "-o",
424                         help="Output directory (relative to current directory)",
425                         type=str, default='output')
426     parser.add_argument("--buildrootdir", "-b",
427                         help="Buildroot directory (relative to current directory)",
428                         type=str, default='.')
429     parser.add_argument("--toolchains-csv",
430                         help="Path of the toolchain configuration file",
431                         type=str,
432                         default="support/config-fragments/autobuild/toolchain-configs.csv")
433     args = parser.parse_args()
434
435     # We need the absolute path to use with O=, because the relative
436     # path to the output directory here is not relative to the
437     # Buildroot sources, but to the current directory.
438     args.outputdir = os.path.abspath(args.outputdir)
439
440     try:
441         ret = gen_config(args)
442     except Exception as e:
443         print(str(e), file=sys.stderr)
444         parser.exit(1)
445     parser.exit(ret)
This page took 0.057544 seconds and 4 git commands to generate.