diff options
Diffstat (limited to 'tools/buildman')
-rw-r--r-- | tools/buildman/.gitignore | 1 | ||||
-rw-r--r-- | tools/buildman/README | 679 | ||||
-rw-r--r-- | tools/buildman/board.py | 167 | ||||
-rw-r--r-- | tools/buildman/bsettings.py | 60 | ||||
-rw-r--r-- | tools/buildman/builder.py | 1445 | ||||
l--------- | tools/buildman/buildman | 1 | ||||
-rwxr-xr-x | tools/buildman/buildman.py | 126 | ||||
-rw-r--r-- | tools/buildman/control.py | 181 | ||||
-rw-r--r-- | tools/buildman/test.py | 185 | ||||
-rw-r--r-- | tools/buildman/toolchain.py | 185 |
10 files changed, 3030 insertions, 0 deletions
diff --git a/tools/buildman/.gitignore b/tools/buildman/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/tools/buildman/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/tools/buildman/README b/tools/buildman/README new file mode 100644 index 0000000..7221007 --- /dev/null +++ b/tools/buildman/README @@ -0,0 +1,679 @@ +# Copyright (c) 2013 The Chromium OS Authors. +# +# See file CREDITS for list of people who contributed to this +# project. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# + +What is this? +============= + +This tool handles building U-Boot to check that you have not broken it +with your patch series. It can build each individual commit and report +which boards fail on which commits, and which errors come up. It aims +to make full use of multi-processor machines. + +A key feature of buildman is its output summary, which allows warnings, +errors or image size increases in a particular commit or board to be +quickly identified and the offending commit pinpointed. This can be a big +help for anyone working with >10 patches at a time. + + +Caveats +======= + +Buildman is still in its infancy. It is already a very useful tool, but +expect to find problems and send patches. + +Buildman can be stopped and restarted, in which case it will continue +where it left off. This should happen cleanly and without side-effects. +If not, it is a bug, for which a patch would be welcome. + +Buildman gets so tied up in its work that it can ignore the outside world. +You may need to press Ctrl-C several times to quit it. Also it will print +out various exceptions when stopped. + + +Theory of Operation +=================== + +(please read this section in full twice or you will be perpetually confused) + +Buildman is a builder. It is not make, although it runs make. It does not +produce any useful output on the terminal while building, except for +progress information. All the output (errors, warnings and binaries if you +are ask for them) is stored in output directories, which you can look at +while the build is progressing, or when it is finished. + +Buildman produces a concise summary of which boards succeeded and failed. +It shows which commit introduced which board failure using a simple +red/green colour coding. Full error information can be requested, in which +case it is de-duped and displayed against the commit that introduced the +error. An example workflow is below. + +Buildman stores image size information and can report changes in image size +from commit to commit. An example of this is below. + +Buildman starts multiple threads, and each thread builds for one board at +a time. A thread starts at the first commit, configures the source for your +board and builds it. Then it checks out the next commit and does an +incremental build. Eventually the thread reaches the last commit and stops. +If errors or warnings are found along the way, the thread will reconfigure +after every commit, and your build will be very slow. This is because a +file that produces just a warning would not normally be rebuilt in an +incremental build. + +Buildman works in an entirely separate place from your U-Boot repository. +It creates a separate working directory for each thread, and puts the +output files in the working directory, organised by commit name and board +name, in a two-level hierarchy. + +Buildman is invoked in your U-Boot directory, the one with the .git +directory. It clones this repository into a copy for each thread, and the +threads do not affect the state of your git repository. Any checkouts done +by the thread affect only the working directory for that thread. + +Buildman automatically selects the correct toolchain for each board. You +must supply suitable toolchains, but buildman takes care of selecting the +right one. + +Buildman always builds a branch, and always builds the upstream commit as +well, for comparison. It cannot build individual commits at present, unless +(maybe) you point it at an empty branch. Put all your commits in a branch, +set the branch's upstream to a valid value, and all will be well. Otherwise +buildman will perform random actions. Use -n to check what the random +actions might be. + +Buildman is optimised for building many commits at once, for many boards. +On multi-core machines, Buildman is fast because it uses most of the +available CPU power. When it gets to the end, or if you are building just +a few commits or boards, it will be pretty slow. As a tip, if you don't +plan to use your machine for anything else, you can use -T to increase the +number of threads beyond the default. + +Buildman lets you build all boards, or a subset. Specify the subset using +the board name, architecture name, SOC name, or anything else in the +boards.cfg file. So 'at91' will build all AT91 boards (arm), powerpc will +build all PowerPC boards. + +Buildman does not store intermediate object files. It optionally copies +the binary output into a directory when a build is successful. Size +information is always recorded. It needs a fair bit of disk space to work, +typically 250MB per thread. + + +Setting up +========== + +1. Get the U-Boot source. You probably already have it, but if not these +steps should get you started with a repo and some commits for testing. + +$ cd /path/to/u-boot +$ git clone git://git.denx.de/u-boot.git . +$ git checkout -b my-branch origin/master +$ # Add some commits to the branch, reading for testing + +2. Create ~/.buildman to tell buildman where to find tool chains. As an +example: + +# Buildman settings file + +[toolchain] +root: / +rest: /toolchains/* +eldk: /opt/eldk-4.2 + +[toolchain-alias] +x86: i386 +blackfin: bfin +sh: sh4 +nds32: nds32le +openrisc: or32 + + +This selects the available toolchain paths. Add the base directory for +each of your toolchains here. Buildman will search inside these directories +and also in any '/usr' and '/usr/bin' subdirectories. + +Make sure the tags (here root: rest: and eldk:) are unique. + +The toolchain-alias section indicates that the i386 toolchain should be used +to build x86 commits. + + +2. Check the available toolchains + +Run this check to make sure that you have a toolchain for every architecture. + +$ ./tools/buildman/buildman --list-tool-chains +Scanning for tool chains + - scanning path '/' + - looking in '/.' + - looking in '/bin' + - looking in '/usr/bin' + - found '/usr/bin/gcc' +Tool chain test: OK + - found '/usr/bin/c89-gcc' +Tool chain test: OK + - found '/usr/bin/c99-gcc' +Tool chain test: OK + - found '/usr/bin/x86_64-linux-gnu-gcc' +Tool chain test: OK + - scanning path '/toolchains/powerpc-linux' + - looking in '/toolchains/powerpc-linux/.' + - looking in '/toolchains/powerpc-linux/bin' + - found '/toolchains/powerpc-linux/bin/powerpc-linux-gcc' +Tool chain test: OK + - looking in '/toolchains/powerpc-linux/usr/bin' + - scanning path '/toolchains/nds32le-linux-glibc-v1f' + - looking in '/toolchains/nds32le-linux-glibc-v1f/.' + - looking in '/toolchains/nds32le-linux-glibc-v1f/bin' + - found '/toolchains/nds32le-linux-glibc-v1f/bin/nds32le-linux-gcc' +Tool chain test: OK + - looking in '/toolchains/nds32le-linux-glibc-v1f/usr/bin' + - scanning path '/toolchains/nios2' + - looking in '/toolchains/nios2/.' + - looking in '/toolchains/nios2/bin' + - found '/toolchains/nios2/bin/nios2-linux-gcc' +Tool chain test: OK + - found '/toolchains/nios2/bin/nios2-linux-uclibc-gcc' +Tool chain test: OK + - looking in '/toolchains/nios2/usr/bin' + - found '/toolchains/nios2/usr/bin/nios2-linux-gcc' +Tool chain test: OK + - found '/toolchains/nios2/usr/bin/nios2-linux-uclibc-gcc' +Tool chain test: OK + - scanning path '/toolchains/microblaze-unknown-linux-gnu' + - looking in '/toolchains/microblaze-unknown-linux-gnu/.' + - looking in '/toolchains/microblaze-unknown-linux-gnu/bin' + - found '/toolchains/microblaze-unknown-linux-gnu/bin/microblaze-unknown-linux-gnu-gcc' +Tool chain test: OK + - found '/toolchains/microblaze-unknown-linux-gnu/bin/mb-linux-gcc' +Tool chain test: OK + - looking in '/toolchains/microblaze-unknown-linux-gnu/usr/bin' + - scanning path '/toolchains/mips-linux' + - looking in '/toolchains/mips-linux/.' + - looking in '/toolchains/mips-linux/bin' + - found '/toolchains/mips-linux/bin/mips-linux-gcc' +Tool chain test: OK + - looking in '/toolchains/mips-linux/usr/bin' + - scanning path '/toolchains/old' + - looking in '/toolchains/old/.' + - looking in '/toolchains/old/bin' + - looking in '/toolchains/old/usr/bin' + - scanning path '/toolchains/i386-linux' + - looking in '/toolchains/i386-linux/.' + - looking in '/toolchains/i386-linux/bin' + - found '/toolchains/i386-linux/bin/i386-linux-gcc' +Tool chain test: OK + - looking in '/toolchains/i386-linux/usr/bin' + - scanning path '/toolchains/bfin-uclinux' + - looking in '/toolchains/bfin-uclinux/.' + - looking in '/toolchains/bfin-uclinux/bin' + - found '/toolchains/bfin-uclinux/bin/bfin-uclinux-gcc' +Tool chain test: OK + - looking in '/toolchains/bfin-uclinux/usr/bin' + - scanning path '/toolchains/sparc-elf' + - looking in '/toolchains/sparc-elf/.' + - looking in '/toolchains/sparc-elf/bin' + - found '/toolchains/sparc-elf/bin/sparc-elf-gcc' +Tool chain test: OK + - looking in '/toolchains/sparc-elf/usr/bin' + - scanning path '/toolchains/arm-2010q1' + - looking in '/toolchains/arm-2010q1/.' + - looking in '/toolchains/arm-2010q1/bin' + - found '/toolchains/arm-2010q1/bin/arm-none-linux-gnueabi-gcc' +Tool chain test: OK + - looking in '/toolchains/arm-2010q1/usr/bin' + - scanning path '/toolchains/from' + - looking in '/toolchains/from/.' + - looking in '/toolchains/from/bin' + - looking in '/toolchains/from/usr/bin' + - scanning path '/toolchains/sh4-gentoo-linux-gnu' + - looking in '/toolchains/sh4-gentoo-linux-gnu/.' + - looking in '/toolchains/sh4-gentoo-linux-gnu/bin' + - found '/toolchains/sh4-gentoo-linux-gnu/bin/sh4-gentoo-linux-gnu-gcc' +Tool chain test: OK + - looking in '/toolchains/sh4-gentoo-linux-gnu/usr/bin' + - scanning path '/toolchains/avr32-linux' + - looking in '/toolchains/avr32-linux/.' + - looking in '/toolchains/avr32-linux/bin' + - found '/toolchains/avr32-linux/bin/avr32-gcc' +Tool chain test: OK + - looking in '/toolchains/avr32-linux/usr/bin' + - scanning path '/toolchains/m68k-linux' + - looking in '/toolchains/m68k-linux/.' + - looking in '/toolchains/m68k-linux/bin' + - found '/toolchains/m68k-linux/bin/m68k-linux-gcc' +Tool chain test: OK + - looking in '/toolchains/m68k-linux/usr/bin' +List of available toolchains (17): +arm : /toolchains/arm-2010q1/bin/arm-none-linux-gnueabi-gcc +avr32 : /toolchains/avr32-linux/bin/avr32-gcc +bfin : /toolchains/bfin-uclinux/bin/bfin-uclinux-gcc +c89 : /usr/bin/c89-gcc +c99 : /usr/bin/c99-gcc +i386 : /toolchains/i386-linux/bin/i386-linux-gcc +m68k : /toolchains/m68k-linux/bin/m68k-linux-gcc +mb : /toolchains/microblaze-unknown-linux-gnu/bin/mb-linux-gcc +microblaze: /toolchains/microblaze-unknown-linux-gnu/bin/microblaze-unknown-linux-gnu-gcc +mips : /toolchains/mips-linux/bin/mips-linux-gcc +nds32le : /toolchains/nds32le-linux-glibc-v1f/bin/nds32le-linux-gcc +nios2 : /toolchains/nios2/bin/nios2-linux-gcc +powerpc : /toolchains/powerpc-linux/bin/powerpc-linux-gcc +sandbox : /usr/bin/gcc +sh4 : /toolchains/sh4-gentoo-linux-gnu/bin/sh4-gentoo-linux-gnu-gcc +sparc : /toolchains/sparc-elf/bin/sparc-elf-gcc +x86_64 : /usr/bin/x86_64-linux-gnu-gcc + + +You can see that everything is covered, even some strange ones that won't +be used (c88 and c99). This is a feature. + + +How to run it +============= + +First do a dry run using the -n flag: (replace <branch> with a real, local +branch with a valid upstream) + +$ ./tools/buildman/buildman -b <branch> -n + +If it can't detect the upstream branch, try checking out the branch, and +doing something like 'git branch --set-upstream <branch> upstream/master' +or something similar. + +As an exmmple: + +Dry run, so not doing much. But I would do this: + +Building 18 commits for 1059 boards (4 threads, 1 job per thread) +Build directory: ../lcd9b + 5bb3505 Merge branch 'master' of git://git.denx.de/u-boot-arm + c18f1b4 tegra: Use const for pinmux_config_pingroup/table() + 2f043ae tegra: Add display support to funcmux + e349900 tegra: fdt: Add pwm binding and node + 424a5f0 tegra: fdt: Add LCD definitions for Tegra + 0636ccf tegra: Add support for PWM + a994fe7 tegra: Add SOC support for display/lcd + fcd7350 tegra: Add LCD driver + 4d46e9d tegra: Add LCD support to Nvidia boards + 991bd48 arm: Add control over cachability of memory regions + 54e8019 lcd: Add CONFIG_LCD_ALIGNMENT to select frame buffer alignment + d92aff7 lcd: Add support for flushing LCD fb from dcache after update + dbd0677 tegra: Align LCD frame buffer to section boundary + 0cff9b8 tegra: Support control of cache settings for LCD + 9c56900 tegra: fdt: Add LCD definitions for Seaboard + 5cc29db lcd: Add CONFIG_CONSOLE_SCROLL_LINES option to speed console + cac5a23 tegra: Enable display/lcd support on Seaboard + 49ff541 wip + +Total boards to build for each commit: 1059 + +This shows that it will build all 1059 boards, using 4 threads (because +we have a 4-core CPU). Each thread will run with -j1, meaning that each +make job will use a single CPU. The list of commits to be built helps you +confirm that things look about right. Notice that buildman has chosen a +'base' directory for you, immediately above your source tree. + +Buildman works entirely inside the base directory, here ../lcd9b, +creating a working directory for each thread, and creating output +directories for each commit and board. + + +Suggested Workflow +================== + +To run the build for real, take off the -n: + +$ ./tools/buildman/buildman -b <branch> + +Buildman will set up some working directories, and get started. After a +minute or so it will settle down to a steady pace, with a display like this: + +Building 18 commits for 1059 boards (4 threads, 1 job per thread) + 528 36 124 /19062 1:13:30 : SIMPC8313_SP + +This means that it is building 19062 board/commit combinations. So far it +has managed to succesfully build 528. Another 36 have built with warnings, +and 124 more didn't build at all. Buildman expects to complete the process +in an hour and 15 minutes. Use this time to buy a faster computer. + + +To find out how the build went, ask for a summary with -s. You can do this +either before the build completes (presumably in another terminal) or or +afterwards. Let's work through an example of how this is used: + +$ ./tools/buildman/buildman -b lcd9b -s +... +01: Merge branch 'master' of git://git.denx.de/u-boot-arm + powerpc: + galaxy5200_LOWBOOT +02: tegra: Use const for pinmux_config_pingroup/table() +03: tegra: Add display support to funcmux +04: tegra: fdt: Add pwm binding and node +05: tegra: fdt: Add LCD definitions for Tegra +06: tegra: Add support for PWM +07: tegra: Add SOC support for display/lcd +08: tegra: Add LCD driver +09: tegra: Add LCD support to Nvidia boards +10: arm: Add control over cachability of memory regions +11: lcd: Add CONFIG_LCD_ALIGNMENT to select frame buffer alignment +12: lcd: Add support for flushing LCD fb from dcache after update + arm: + lubbock +13: tegra: Align LCD frame buffer to section boundary +14: tegra: Support control of cache settings for LCD +15: tegra: fdt: Add LCD definitions for Seaboard +16: lcd: Add CONFIG_CONSOLE_SCROLL_LINES option to speed console +17: tegra: Enable display/lcd support on Seaboard +18: wip + +This shows which commits have succeeded and which have failed. In this case +the build is still in progress so many boards are not built yet (use -u to +see which ones). But still we can see a few failures. The galaxy5200_LOWBOOT +never builds correctly. This could be a problem with our toolchain, or it +could be a bug in the upstream. The good news is that we probably don't need +to blame our commits. The bad news is it isn't tested on that board. + +Commit 12 broke lubbock. That's what the '+ lubbock' means. The failure +is never fixed by a later commit, or you would see lubbock again, in green, +without the +. + +To see the actual error: + +$ ./tools/buildman/buildman -b <branch> -se lubbock +... +12: lcd: Add support for flushing LCD fb from dcache after update + arm: + lubbock ++common/libcommon.o: In function `lcd_sync': ++/u-boot/lcd9b/.bm-work/00/common/lcd.c:120: undefined reference to `flush_dcache_range' ++arm-none-linux-gnueabi-ld: BFD (Sourcery G++ Lite 2010q1-202) 2.19.51.20090709 assertion fail /scratch/julian/2010q1-release-linux-lite/obj/binutils-src-2010q1-202-arm-none-linux-gnueabi-i686-pc-linux-gnu/bfd/elf32-arm.c:12572 ++make: *** [/u-boot/lcd9b/.bm-work/00/build/u-boot] Error 139 +13: tegra: Align LCD frame buffer to section boundary +14: tegra: Support control of cache settings for LCD +15: tegra: fdt: Add LCD definitions for Seaboard +16: lcd: Add CONFIG_CONSOLE_SCROLL_LINES option to speed console +-/u-boot/lcd9b/.bm-work/00/common/lcd.c:120: undefined reference to `flush_dcache_range' ++/u-boot/lcd9b/.bm-work/00/common/lcd.c:125: undefined reference to `flush_dcache_range' +17: tegra: Enable display/lcd support on Seaboard +18: wip + +So the problem is in lcd.c, due to missing cache operations. This information +should be enough to work out what that commit is doing to break these +boards. (In this case pxa did not have cache operations defined). + +If you see error lines marked with - that means that the errors were fixed +by that commit. Sometimes commits can be in the wrong order, so that a +breakage is introduced for a few commits and fixed by later commits. This +shows up clearly with buildman. You can then reorder the commits and try +again. + +At commit 16, the error moves - you can see that the old error at line 120 +is fixed, but there is a new one at line 126. This is probably only because +we added some code and moved the broken line futher down the file. + +If many boards have the same error, then -e will display the error only +once. This makes the output as concise as possible. + +The full build output in this case is available in: + +../lcd9b/12_of_18_gd92aff7_lcd--Add-support-for/lubbock/ + + done: Indicates the build was done, and holds the return code from make. + This is 0 for a good build, typically 2 for a failure. + + err: Output from stderr, if any. Errors and warnings appear here. + + log: Output from stdout. Normally there isn't any since buildman runs + in silent mode for now. + + toolchain: Shows information about the toolchain used for the build. + + sizes: Shows image size information. + +It is possible to get the build output there also. Use the -k option for +this. In that case you will also see some output files, like: + + System.map toolchain u-boot u-boot.bin u-boot.map autoconf.mk + (also SPL versions u-boot-spl and u-boot-spl.bin if available) + + +Checking Image Sizes +==================== + +A key requirement for U-Boot is that you keep code/data size to a minimum. +Where a new feature increases this noticeably it should normally be put +behind a CONFIG flag so that boards can leave it off and keep the image +size more or less the same with each new release. + +To check the impact of your commits on image size, use -S. For example: + +$ ./tools/buildman/buildman -b us-x86 -sS +Summary of 10 commits for 1066 boards (4 threads, 1 job per thread) +01: MAKEALL: add support for per architecture toolchains +02: x86: Add function to get top of usable ram + x86: (for 1/3 boards) text -272.0 rodata +41.0 +03: x86: Add basic cache operations +04: x86: Permit bootstage and timer data to be used prior to relocation + x86: (for 1/3 boards) data +16.0 +05: x86: Add an __end symbol to signal the end of the U-Boot binary + x86: (for 1/3 boards) text +76.0 +06: x86: Rearrange the output input to remove BSS + x86: (for 1/3 boards) bss -2140.0 +07: x86: Support relocation of FDT on start-up + x86: + coreboot-x86 +08: x86: Add error checking to x86 relocation code +09: x86: Adjust link device tree include file +10: x86: Enable CONFIG_OF_CONTROL on coreboot + + +You can see that image size only changed on x86, which is good because this +series is not supposed to change any other board. From commit 7 onwards the +build fails so we don't get code size numbers. The numbers are fractional +because they are an average of all boards for that architecture. The +intention is to allow you to quickly find image size problems introduced by +your commits. + +Note that the 'text' region and 'rodata' are split out. You should add the +two together to get the total read-only size (reported as the first column +in the output from binutil's 'size' utility). + +A useful option is --step which lets you skip some commits. For example +--step 2 will show the image sizes for only every 2nd commit (so it will +compare the image sizes of the 1st, 3rd, 5th... commits). You can also use +--step 0 which will compare only the first and last commits. This is useful +for an overview of how your entire series affects code size. + +You can also use -d to see a detailed size breakdown for each board. This +list is sorted in order from largest growth to largest reduction. + +It is possible to go a little further with the -B option (--bloat). This +shows where U-Boot has bloted, breaking the size change down to the function +level. Example output is below: + +$ ./tools/buildman/buildman -b us-mem4 -sSdB +... +19: Roll crc32 into hash infrastructure + arm: (for 10/10 boards) all -143.4 bss +1.2 data -4.8 rodata -48.2 text -91.6 + paz00 : all +23 bss -4 rodata -29 text +56 + u-boot: add: 1/0, grow: 3/-2 bytes: 168/-104 (64) + function old new delta + hash_command 80 160 +80 + crc32_wd_buf - 56 +56 + ext4fs_read_file 540 568 +28 + insert_var_value_sub 688 692 +4 + run_list_real 1996 1992 -4 + do_mem_crc 168 68 -100 + trimslice : all -9 bss +16 rodata -29 text +4 + u-boot: add: 1/0, grow: 1/-3 bytes: 136/-124 (12) + function old new delta + hash_command 80 160 +80 + crc32_wd_buf - 56 +56 + ext4fs_iterate_dir 672 668 -4 + ext4fs_read_file 568 548 -20 + do_mem_crc 168 68 -100 + whistler : all -9 bss +16 rodata -29 text +4 + u-boot: add: 1/0, grow: 1/-3 bytes: 136/-124 (12) + function old new delta + hash_command 80 160 +80 + crc32_wd_buf - 56 +56 + ext4fs_iterate_dir 672 668 -4 + ext4fs_read_file 568 548 -20 + do_mem_crc 168 68 -100 + seaboard : all -9 bss -28 rodata -29 text +48 + u-boot: add: 1/0, grow: 3/-2 bytes: 160/-104 (56) + function old new delta + hash_command 80 160 +80 + crc32_wd_buf - 56 +56 + ext4fs_read_file 548 568 +20 + run_list_real 1996 2000 +4 + do_nandboot 760 756 -4 + do_mem_crc 168 68 -100 + colibri_t20_iris: all -9 rodata -29 text +20 + u-boot: add: 1/0, grow: 2/-3 bytes: 140/-112 (28) + function old new delta + hash_command 80 160 +80 + crc32_wd_buf - 56 +56 + read_abs_bbt 204 208 +4 + do_nandboot 760 756 -4 + ext4fs_read_file 576 568 -8 + do_mem_crc 168 68 -100 + ventana : all -37 bss -12 rodata -29 text +4 + u-boot: add: 1/0, grow: 1/-3 bytes: 136/-124 (12) + function old new delta + hash_command 80 160 +80 + crc32_wd_buf - 56 +56 + ext4fs_iterate_dir 672 668 -4 + ext4fs_read_file 568 548 -20 + do_mem_crc 168 68 -100 + harmony : all -37 bss -16 rodata -29 text +8 + u-boot: add: 1/0, grow: 2/-3 bytes: 140/-124 (16) + function old new delta + hash_command 80 160 +80 + crc32_wd_buf - 56 +56 + nand_write_oob_syndrome 428 432 +4 + ext4fs_iterate_dir 672 668 -4 + ext4fs_read_file 568 548 -20 + do_mem_crc 168 68 -100 + medcom-wide : all -417 bss +28 data -16 rodata -93 text -336 + u-boot: add: 1/-1, grow: 1/-2 bytes: 88/-376 (-288) + function old new delta + crc32_wd_buf - 56 +56 + do_fat_read_at 2872 2904 +32 + hash_algo 16 - -16 + do_mem_crc 168 68 -100 + hash_command 420 160 -260 + tec : all -449 bss -4 data -16 rodata -93 text -336 + u-boot: add: 1/-1, grow: 1/-2 bytes: 88/-376 (-288) + function old new delta + crc32_wd_buf - 56 +56 + do_fat_read_at 2872 2904 +32 + hash_algo 16 - -16 + do_mem_crc 168 68 -100 + hash_command 420 160 -260 + plutux : all -481 bss +16 data -16 rodata -93 text -388 + u-boot: add: 1/-1, grow: 1/-3 bytes: 68/-408 (-340) + function old new delta + crc32_wd_buf - 56 +56 + do_load_serial_bin 1688 1700 +12 + hash_algo 16 - -16 + do_fat_read_at 2904 2872 -32 + do_mem_crc 168 68 -100 + hash_command 420 160 -260 + powerpc: (for 5/5 boards) all +37.4 data -3.2 rodata -41.8 text +82.4 + MPC8610HPCD : all +55 rodata -29 text +84 + u-boot: add: 1/0, grow: 0/-1 bytes: 176/-96 (80) + function old new delta + hash_command - 176 +176 + do_mem_crc 184 88 -96 + MPC8641HPCN : all +55 rodata -29 text +84 + u-boot: add: 1/0, grow: 0/-1 bytes: 176/-96 (80) + function old new delta + hash_command - 176 +176 + do_mem_crc 184 88 -96 + MPC8641HPCN_36BIT: all +55 rodata -29 text +84 + u-boot: add: 1/0, grow: 0/-1 bytes: 176/-96 (80) + function old new delta + hash_command - 176 +176 + do_mem_crc 184 88 -96 + sbc8641d : all +55 rodata -29 text +84 + u-boot: add: 1/0, grow: 0/-1 bytes: 176/-96 (80) + function old new delta + hash_command - 176 +176 + do_mem_crc 184 88 -96 + xpedite517x : all -33 data -16 rodata -93 text +76 + u-boot: add: 1/-1, grow: 0/-1 bytes: 176/-112 (64) + function old new delta + hash_command - 176 +176 + hash_algo 16 - -16 + do_mem_crc 184 88 -96 +... + + +This shows that commit 19 has increased text size for arm (although only one +board was built) and by 96 bytes for powerpc. This increase was offset in both +cases by reductions in rodata and data/bss. + +Shown below the summary lines is the sizes for each board. Below each board +is the sizes for each function. This information starts with: + + add - number of functions added / removed + grow - number of functions which grew / shrunk + bytes - number of bytes of code added to / removed from all functions, + plus the total byte change in brackets + +The change seems to be that hash_command() has increased by more than the +do_mem_crc() function has decreased. The function sizes typically add up to +roughly the text area size, but note that every read-only section except +rodata is included in 'text', so the function total does not exactly +correspond. + +It is common when refactoring code for the rodata to decrease as the text size +increases, and vice versa. + + +Other options +============= + +Buildman has various other command line options. Try --help to see them. + + +TODO +==== + +This has mostly be written in my spare time as a response to my difficulties +in testing large series of patches. Apart from tidying up there is quite a +bit of scope for improvement. Things like better error diffs, easier access +to log files, error display while building. Also it would be nice it buildman +could 'hunt' for problems, perhaps by building a few boards for each arch, +or checking commits for changed files and building only boards which use +those files. + + +Credits +======= + +Thanks to Grant Grundler <grundler@chromium.org> for his ideas for improving +the build speed by building all commits for a board instead of the other +way around. + + + +Simon Glass +sjg@chromium.org +Halloween 2012 +Updated 12-12-12 +Updated 23-02-13 diff --git a/tools/buildman/board.py b/tools/buildman/board.py new file mode 100644 index 0000000..9f50aba --- /dev/null +++ b/tools/buildman/board.py @@ -0,0 +1,167 @@ +# Copyright (c) 2012 The Chromium OS Authors. +# +# See file CREDITS for list of people who contributed to this +# project. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# + +class Board: + """A particular board that we can build""" + def __init__(self, target, arch, cpu, board_name, vendor, soc, options): + """Create a new board type. + + Args: + target: Target name (use make <target>_config to configure) + arch: Architecture name (e.g. arm) + cpu: Cpu name (e.g. arm1136) + board_name: Name of board (e.g. integrator) + vendor: Name of vendor (e.g. armltd) + soc: Name of SOC, or '' if none (e.g. mx31) + options: board-specific options (e.g. integratorcp:CM1136) + """ + self.target = target + self.arch = arch + self.cpu = cpu + self.board_name = board_name + self.vendor = vendor + self.soc = soc + self.props = [self.target, self.arch, self.cpu, self.board_name, + self.vendor, self.soc] + self.options = options + self.build_it = False + + +class Boards: + """Manage a list of boards.""" + def __init__(self): + # Use a simple list here, sinc OrderedDict requires Python 2.7 + self._boards = [] + + def AddBoard(self, board): + """Add a new board to the list. + + The board's target member must not already exist in the board list. + + Args: + board: board to add + """ + self._boards.append(board) + + def ReadBoards(self, fname): + """Read a list of boards from a board file. + + Create a board object for each and add it to our _boards list. + + Args: + fname: Filename of boards.cfg file + """ + with open(fname, 'r') as fd: + for line in fd: + if line[0] == '#': + continue + fields = line.split() + if not fields: + continue + for upto in range(len(fields)): + if fields[upto] == '-': + fields[upto] = '' + while len(fields) < 7: + fields.append('') + + board = Board(*fields) + self.AddBoard(board) + + + def GetList(self): + """Return a list of available boards. + + Returns: + List of Board objects + """ + return self._boards + + def GetDict(self): + """Build a dictionary containing all the boards. + + Returns: + Dictionary: + key is board.target + value is board + """ + board_dict = {} + for board in self._boards: + board_dict[board.target] = board + return board_dict + + def GetSelectedDict(self): + """Return a dictionary containing the selected boards + + Returns: + List of Board objects that are marked selected + """ + board_dict = {} + for board in self._boards: + if board.build_it: + board_dict[board.target] = board + return board_dict + + def GetSelected(self): + """Return a list of selected boards + + Returns: + List of Board objects that are marked selected + """ + return [board for board in self._boards if board.build_it] + + def GetSelectedNames(self): + """Return a list of selected boards + + Returns: + List of board names that are marked selected + """ + return [board.target for board in self._boards if board.build_it] + + def SelectBoards(self, args): + """Mark boards selected based on args + + Args: + List of strings specifying boards to include, either named, or + by their target, architecture, cpu, vendor or soc. If empty, all + boards are selected. + + Returns: + Dictionary which holds the number of boards which were selected + due to each argument, arranged by argument. + """ + result = {} + for arg in args: + result[arg] = 0 + result['all'] = 0 + + for board in self._boards: + if args: + for arg in args: + if arg in board.props: + if not board.build_it: + board.build_it = True + result[arg] += 1 + result['all'] += 1 + else: + board.build_it = True + result['all'] += 1 + + return result diff --git a/tools/buildman/bsettings.py b/tools/buildman/bsettings.py new file mode 100644 index 0000000..7e66c63 --- /dev/null +++ b/tools/buildman/bsettings.py @@ -0,0 +1,60 @@ +# Copyright (c) 2012 The Chromium OS Authors. +# +# See file CREDITS for list of people who contributed to this +# project. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# + +import ConfigParser +import os + + +def Setup(fname=''): + """Set up the buildman settings module by reading config files + + Args: + config_fname: Config filename to read ('' for default) + """ + global settings + global config_fname + + settings = ConfigParser.SafeConfigParser() + config_fname = fname + if config_fname == '': + config_fname = '%s/.buildman' % os.getenv('HOME') + if config_fname: + settings.read(config_fname) + +def GetItems(section): + """Get the items from a section of the config. + + Args: + section: name of section to retrieve + + Returns: + List of (name, value) tuples for the section + """ + try: + return settings.items(section) + except ConfigParser.NoSectionError as e: + print e + print ("Warning: No tool chains - please add a [toolchain] section " + "to your buildman config file %s. See README for details" % + config_fname) + return [] + except: + raise diff --git a/tools/buildman/builder.py b/tools/buildman/builder.py new file mode 100644 index 0000000..e426442 --- /dev/null +++ b/tools/buildman/builder.py @@ -0,0 +1,1445 @@ +# Copyright (c) 2013 The Chromium OS Authors. +# +# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com> +# +# See file CREDITS for list of people who contributed to this +# project. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# + +import collections +import errno +from datetime import datetime, timedelta +import glob +import os +import re +import Queue +import shutil +import string +import sys +import threading +import time + +import command +import gitutil +import terminal +import toolchain + + +""" +Theory of Operation + +Please see README for user documentation, and you should be familiar with +that before trying to make sense of this. + +Buildman works by keeping the machine as busy as possible, building different +commits for different boards on multiple CPUs at once. + +The source repo (self.git_dir) contains all the commits to be built. Each +thread works on a single board at a time. It checks out the first commit, +configures it for that board, then builds it. Then it checks out the next +commit and builds it (typically without re-configuring). When it runs out +of commits, it gets another job from the builder and starts again with that +board. + +Clearly the builder threads could work either way - they could check out a +commit and then built it for all boards. Using separate directories for each +commit/board pair they could leave their build product around afterwards +also. + +The intent behind building a single board for multiple commits, is to make +use of incremental builds. Since each commit is built incrementally from +the previous one, builds are faster. Reconfiguring for a different board +removes all intermediate object files. + +Many threads can be working at once, but each has its own working directory. +When a thread finishes a build, it puts the output files into a result +directory. + +The base directory used by buildman is normally '../<branch>', i.e. +a directory higher than the source repository and named after the branch +being built. + +Within the base directory, we have one subdirectory for each commit. Within +that is one subdirectory for each board. Within that is the build output for +that commit/board combination. + +Buildman also create working directories for each thread, in a .bm-work/ +subdirectory in the base dir. + +As an example, say we are building branch 'us-net' for boards 'sandbox' and +'seaboard', and say that us-net has two commits. We will have directories +like this: + +us-net/ base directory + 01_of_02_g4ed4ebc_net--Add-tftp-speed-/ + sandbox/ + u-boot.bin + seaboard/ + u-boot.bin + 02_of_02_g4ed4ebc_net--Check-tftp-comp/ + sandbox/ + u-boot.bin + seaboard/ + u-boot.bin + .bm-work/ + 00/ working directory for thread 0 (contains source checkout) + build/ build output + 01/ working directory for thread 1 + build/ build output + ... +u-boot/ source directory + .git/ repository +""" + +# Possible build outcomes +OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4) + +# Translate a commit subject into a valid filename +trans_valid_chars = string.maketrans("/: ", "---") + + +def Mkdir(dirname): + """Make a directory if it doesn't already exist. + + Args: + dirname: Directory to create + """ + try: + os.mkdir(dirname) + except OSError as err: + if err.errno == errno.EEXIST: + pass + else: + raise + +class BuilderJob: + """Holds information about a job to be performed by a thread + + Members: + board: Board object to build + commits: List of commit options to build. + """ + def __init__(self): + self.board = None + self.commits = [] + + +class ResultThread(threading.Thread): + """This thread processes results from builder threads. + + It simply passes the results on to the builder. There is only one + result thread, and this helps to serialise the build output. + """ + def __init__(self, builder): + """Set up a new result thread + + Args: + builder: Builder which will be sent each result + """ + threading.Thread.__init__(self) + self.builder = builder + + def run(self): + """Called to start up the result thread. + + We collect the next result job and pass it on to the build. + """ + while True: + result = self.builder.out_queue.get() + self.builder.ProcessResult(result) + self.builder.out_queue.task_done() + + +class BuilderThread(threading.Thread): + """This thread builds U-Boot for a particular board. + + An input queue provides each new job. We run 'make' to build U-Boot + and then pass the results on to the output queue. + + Members: + builder: The builder which contains information we might need + thread_num: Our thread number (0-n-1), used to decide on a + temporary directory + """ + def __init__(self, builder, thread_num): + """Set up a new builder thread""" + threading.Thread.__init__(self) + self.builder = builder + self.thread_num = thread_num + + def Make(self, commit, brd, stage, cwd, *args, **kwargs): + """Run 'make' on a particular commit and board. + + The source code will already be checked out, so the 'commit' + argument is only for information. + + Args: + commit: Commit object that is being built + brd: Board object that is being built + stage: Stage of the build. Valid stages are: + distclean - can be called to clean source + config - called to configure for a board + build - the main make invocation - it does the build + args: A list of arguments to pass to 'make' + kwargs: A list of keyword arguments to pass to command.RunPipe() + + Returns: + CommandResult object + """ + return self.builder.do_make(commit, brd, stage, cwd, *args, + **kwargs) + + def RunCommit(self, commit_upto, brd, work_dir, do_config, force_build): + """Build a particular commit. + + If the build is already done, and we are not forcing a build, we skip + the build and just return the previously-saved results. + + Args: + commit_upto: Commit number to build (0...n-1) + brd: Board object to build + work_dir: Directory to which the source will be checked out + do_config: True to run a make <board>_config on the source + force_build: Force a build even if one was previously done + + Returns: + tuple containing: + - CommandResult object containing the results of the build + - boolean indicating whether 'make config' is still needed + """ + # Create a default result - it will be overwritte by the call to + # self.Make() below, in the event that we do a build. + result = command.CommandResult() + result.return_code = 0 + out_dir = os.path.join(work_dir, 'build') + + # Check if the job was already completed last time + done_file = self.builder.GetDoneFile(commit_upto, brd.target) + result.already_done = os.path.exists(done_file) + if result.already_done and not force_build: + # Get the return code from that build and use it + with open(done_file, 'r') as fd: + result.return_code = int(fd.readline()) + err_file = self.builder.GetErrFile(commit_upto, brd.target) + if os.path.exists(err_file) and os.stat(err_file).st_size: + result.stderr = 'bad' + else: + # We are going to have to build it. First, get a toolchain + if not self.toolchain: + try: + self.toolchain = self.builder.toolchains.Select(brd.arch) + except ValueError as err: + result.return_code = 10 + result.stdout = '' + result.stderr = str(err) + # TODO(sjg@chromium.org): This gets swallowed, but needs + # to be reported. + + if self.toolchain: + # Checkout the right commit + if commit_upto is not None: + commit = self.builder.commits[commit_upto] + if self.builder.checkout: + git_dir = os.path.join(work_dir, '.git') + gitutil.Checkout(commit.hash, git_dir, work_dir, + force=True) + else: + commit = self.builder.commit # Ick, fix this for BuildCommits() + + # Set up the environment and command line + env = self.toolchain.MakeEnvironment() + Mkdir(out_dir) + args = ['O=build', '-s'] + if self.builder.num_jobs is not None: + args.extend(['-j', str(self.builder.num_jobs)]) + config_args = ['%s_config' % brd.target] + config_out = '' + + # If we need to reconfigure, do that now + if do_config: + result = self.Make(commit, brd, 'distclean', work_dir, + 'distclean', *args, env=env) + result = self.Make(commit, brd, 'config', work_dir, + *(args + config_args), env=env) + config_out = result.combined + do_config = False # No need to configure next time + if result.return_code == 0: + result = self.Make(commit, brd, 'build', work_dir, *args, + env=env) + result.stdout = config_out + result.stdout + else: + result.return_code = 1 + result.stderr = 'No tool chain for %s\n' % brd.arch + result.already_done = False + + result.toolchain = self.toolchain + result.brd = brd + result.commit_upto = commit_upto + result.out_dir = out_dir + return result, do_config + + def _WriteResult(self, result, keep_outputs): + """Write a built result to the output directory. + + Args: + result: CommandResult object containing result to write + keep_outputs: True to store the output binaries, False + to delete them + """ + # Fatal error + if result.return_code < 0: + return + + # Aborted? + if result.stderr and 'No child processes' in result.stderr: + return + + if result.already_done: + return + + # Write the output and stderr + output_dir = self.builder._GetOutputDir(result.commit_upto) + Mkdir(output_dir) + build_dir = self.builder.GetBuildDir(result.commit_upto, + result.brd.target) + Mkdir(build_dir) + + outfile = os.path.join(build_dir, 'log') + with open(outfile, 'w') as fd: + if result.stdout: + fd.write(result.stdout) + + errfile = self.builder.GetErrFile(result.commit_upto, + result.brd.target) + if result.stderr: + with open(errfile, 'w') as fd: + fd.write(result.stderr) + elif os.path.exists(errfile): + os.remove(errfile) + + if result.toolchain: + # Write the build result and toolchain information. + done_file = self.builder.GetDoneFile(result.commit_upto, + result.brd.target) + with open(done_file, 'w') as fd: + fd.write('%s' % result.return_code) + with open(os.path.join(build_dir, 'toolchain'), 'w') as fd: + print >>fd, 'gcc', result.toolchain.gcc + print >>fd, 'path', result.toolchain.path + print >>fd, 'cross', result.toolchain.cross + print >>fd, 'arch', result.toolchain.arch + fd.write('%s' % result.return_code) + + with open(os.path.join(build_dir, 'toolchain'), 'w') as fd: + print >>fd, 'gcc', result.toolchain.gcc + print >>fd, 'path', result.toolchain.path + + # Write out the image and function size information and an objdump + env = result.toolchain.MakeEnvironment() + lines = [] + for fname in ['u-boot', 'spl/u-boot-spl']: + cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname] + nm_result = command.RunPipe([cmd], capture=True, + capture_stderr=True, cwd=result.out_dir, + raise_on_error=False, env=env) + if nm_result.stdout: + nm = self.builder.GetFuncSizesFile(result.commit_upto, + result.brd.target, fname) + with open(nm, 'w') as fd: + print >>fd, nm_result.stdout, + + cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname] + dump_result = command.RunPipe([cmd], capture=True, + capture_stderr=True, cwd=result.out_dir, + raise_on_error=False, env=env) + rodata_size = '' + if dump_result.stdout: + objdump = self.builder.GetObjdumpFile(result.commit_upto, + result.brd.target, fname) + with open(objdump, 'w') as fd: + print >>fd, dump_result.stdout, + for line in dump_result.stdout.splitlines(): + fields = line.split() + if len(fields) > 5 and fields[1] == '.rodata': + rodata_size = fields[2] + + cmd = ['%ssize' % self.toolchain.cross, fname] + size_result = command.RunPipe([cmd], capture=True, + capture_stderr=True, cwd=result.out_dir, + raise_on_error=False, env=env) + if size_result.stdout: + lines.append(size_result.stdout.splitlines()[1] + ' ' + + rodata_size) + + # Write out the image sizes file. This is similar to the output + # of binutil's 'size' utility, but it omits the header line and + # adds an additional hex value at the end of each line for the + # rodata size + if len(lines): + sizes = self.builder.GetSizesFile(result.commit_upto, + result.brd.target) + with open(sizes, 'w') as fd: + print >>fd, '\n'.join(lines) + + # Now write the actual build output + if keep_outputs: + patterns = ['u-boot', '*.bin', 'u-boot.dtb', '*.map', + 'include/autoconf.mk', 'spl/u-boot-spl', + 'spl/u-boot-spl.bin'] + for pattern in patterns: + file_list = glob.glob(os.path.join(result.out_dir, pattern)) + for fname in file_list: + shutil.copy(fname, build_dir) + + + def RunJob(self, job): + """Run a single job + + A job consists of a building a list of commits for a particular board. + + Args: + job: Job to build + """ + brd = job.board + work_dir = self.builder.GetThreadDir(self.thread_num) + self.toolchain = None + if job.commits: + # Run 'make board_config' on the first commit + do_config = True + commit_upto = 0 + force_build = False + for commit_upto in range(0, len(job.commits), job.step): + result, request_config = self.RunCommit(commit_upto, brd, + work_dir, do_config, + force_build or self.builder.force_build) + failed = result.return_code or result.stderr + if failed and not do_config: + # If our incremental build failed, try building again + # with a reconfig. + if self.builder.force_config_on_failure: + result, request_config = self.RunCommit(commit_upto, + brd, work_dir, True, True) + do_config = request_config + + # If we built that commit, then config is done. But if we got + # an warning, reconfig next time to force it to build the same + # files that created warnings this time. Otherwise an + # incremental build may not build the same file, and we will + # think that the warning has gone away. + # We could avoid this by using -Werror everywhere... + # For errors, the problem doesn't happen, since presumably + # the build stopped and didn't generate output, so will retry + # that file next time. So we could detect warnings and deal + # with them specially here. For now, we just reconfigure if + # anything goes work. + # Of course this is substantially slower if there are build + # errors/warnings (e.g. 2-3x slower even if only 10% of builds + # have problems). + if (failed and not result.already_done and not do_config and + self.builder.force_config_on_failure): + # If this build failed, try the next one with a + # reconfigure. + # Sometimes if the board_config.h file changes it can mess + # with dependencies, and we get: + # make: *** No rule to make target `include/autoconf.mk', + # needed by `depend'. + do_config = True + force_build = True + else: + force_build = False + if self.builder.force_config_on_failure: + if failed: + do_config = True + result.commit_upto = commit_upto + if result.return_code < 0: + raise ValueError('Interrupt') + + # We have the build results, so output the result + self._WriteResult(result, job.keep_outputs) + self.builder.out_queue.put(result) + else: + # Just build the currently checked-out build + result = self.RunCommit(None, True) + result.commit_upto = self.builder.upto + self.builder.out_queue.put(result) + + def run(self): + """Our thread's run function + + This thread picks a job from the queue, runs it, and then goes to the + next job. + """ + alive = True + while True: + job = self.builder.queue.get() + try: + if self.builder.active and alive: + self.RunJob(job) + except Exception as err: + alive = False + print err + self.builder.queue.task_done() + + +class Builder: + """Class for building U-Boot for a particular commit. + + Public members: (many should ->private) + active: True if the builder is active and has not been stopped + already_done: Number of builds already completed + base_dir: Base directory to use for builder + checkout: True to check out source, False to skip that step. + This is used for testing. + col: terminal.Color() object + count: Number of commits to build + do_make: Method to call to invoke Make + fail: Number of builds that failed due to error + force_build: Force building even if a build already exists + force_config_on_failure: If a commit fails for a board, disable + incremental building for the next commit we build for that + board, so that we will see all warnings/errors again. + git_dir: Git directory containing source repository + last_line_len: Length of the last line we printed (used for erasing + it with new progress information) + num_jobs: Number of jobs to run at once (passed to make as -j) + num_threads: Number of builder threads to run + out_queue: Queue of results to process + re_make_err: Compiled regular expression for ignore_lines + queue: Queue of jobs to run + threads: List of active threads + toolchains: Toolchains object to use for building + upto: Current commit number we are building (0.count-1) + warned: Number of builds that produced at least one warning + + Private members: + _base_board_dict: Last-summarised Dict of boards + _base_err_lines: Last-summarised list of errors + _build_period_us: Time taken for a single build (float object). + _complete_delay: Expected delay until completion (timedelta) + _next_delay_update: Next time we plan to display a progress update + (datatime) + _show_unknown: Show unknown boards (those not built) in summary + _timestamps: List of timestamps for the completion of the last + last _timestamp_count builds. Each is a datetime object. + _timestamp_count: Number of timestamps to keep in our list. + _working_dir: Base working directory containing all threads + """ + class Outcome: + """Records a build outcome for a single make invocation + + Public Members: + rc: Outcome value (OUTCOME_...) + err_lines: List of error lines or [] if none + sizes: Dictionary of image size information, keyed by filename + - Each value is itself a dictionary containing + values for 'text', 'data' and 'bss', being the integer + size in bytes of each section. + func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each + value is itself a dictionary: + key: function name + value: Size of function in bytes + """ + def __init__(self, rc, err_lines, sizes, func_sizes): + self.rc = rc + self.err_lines = err_lines + self.sizes = sizes + self.func_sizes = func_sizes + + def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs, + checkout=True, show_unknown=True, step=1): + """Create a new Builder object + + Args: + toolchains: Toolchains object to use for building + base_dir: Base directory to use for builder + git_dir: Git directory containing source repository + num_threads: Number of builder threads to run + num_jobs: Number of jobs to run at once (passed to make as -j) + checkout: True to check out source, False to skip that step. + This is used for testing. + show_unknown: Show unknown boards (those not built) in summary + step: 1 to process every commit, n to process every nth commit + """ + self.toolchains = toolchains + self.base_dir = base_dir + self._working_dir = os.path.join(base_dir, '.bm-work') + self.threads = [] + self.active = True + self.do_make = self.Make + self.checkout = checkout + self.num_threads = num_threads + self.num_jobs = num_jobs + self.already_done = 0 + self.force_build = False + self.git_dir = git_dir + self._show_unknown = show_unknown + self._timestamp_count = 10 + self._build_period_us = None + self._complete_delay = None + self._next_delay_update = datetime.now() + self.force_config_on_failure = True + self._step = step + + self.col = terminal.Color() + + self.queue = Queue.Queue() + self.out_queue = Queue.Queue() + for i in range(self.num_threads): + t = BuilderThread(self, i) + t.setDaemon(True) + t.start() + self.threads.append(t) + + self.last_line_len = 0 + t = ResultThread(self) + t.setDaemon(True) + t.start() + self.threads.append(t) + + ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)'] + self.re_make_err = re.compile('|'.join(ignore_lines)) + + def __del__(self): + """Get rid of all threads created by the builder""" + for t in self.threads: + del t + + def _AddTimestamp(self): + """Add a new timestamp to the list and record the build period. + + The build period is the length of time taken to perform a single + build (one board, one commit). + """ + now = datetime.now() + self._timestamps.append(now) + count = len(self._timestamps) + delta = self._timestamps[-1] - self._timestamps[0] + seconds = delta.total_seconds() + + # If we have enough data, estimate build period (time taken for a + # single build) and therefore completion time. + if count > 1 and self._next_delay_update < now: + self._next_delay_update = now + timedelta(seconds=2) + if seconds > 0: + self._build_period = float(seconds) / count + todo = self.count - self.upto + self._complete_delay = timedelta(microseconds= + self._build_period * todo * 1000000) + # Round it + self._complete_delay -= timedelta( + microseconds=self._complete_delay.microseconds) + + if seconds > 60: + self._timestamps.popleft() + count -= 1 + + def ClearLine(self, length): + """Clear any characters on the current line + + Make way for a new line of length 'length', by outputting enough + spaces to clear out the old line. Then remember the new length for + next time. + + Args: + length: Length of new line, in characters + """ + if length < self.last_line_len: + print ' ' * (self.last_line_len - length), + print '\r', + self.last_line_len = length + sys.stdout.flush() + + def SelectCommit(self, commit, checkout=True): + """Checkout the selected commit for this build + """ + self.commit = commit + if checkout and self.checkout: + gitutil.Checkout(commit.hash) + + def Make(self, commit, brd, stage, cwd, *args, **kwargs): + """Run make + + Args: + commit: Commit object that is being built + brd: Board object that is being built + stage: Stage that we are at (distclean, config, build) + cwd: Directory where make should be run + args: Arguments to pass to make + kwargs: Arguments to pass to command.RunPipe() + """ + cmd = ['make'] + list(args) + result = command.RunPipe([cmd], capture=True, capture_stderr=True, + cwd=cwd, raise_on_error=False, **kwargs) + return result + + def ProcessResult(self, result): + """Process the result of a build, showing progress information + + Args: + result: A CommandResult object + """ + col = terminal.Color() + if result: + target = result.brd.target + + if result.return_code < 0: + self.active = False + command.StopAll() + return + + self.upto += 1 + if result.return_code != 0: + self.fail += 1 + elif result.stderr: + self.warned += 1 + if result.already_done: + self.already_done += 1 + else: + target = '(starting)' + + # Display separate counts for ok, warned and fail + ok = self.upto - self.warned - self.fail + line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok) + line += self.col.Color(self.col.YELLOW, '%5d' % self.warned) + line += self.col.Color(self.col.RED, '%5d' % self.fail) + + name = ' /%-5d ' % self.count + + # Add our current completion time estimate + self._AddTimestamp() + if self._complete_delay: + name += '%s : ' % self._complete_delay + # When building all boards for a commit, we can print a commit + # progress message. + if result and result.commit_upto is None: + name += 'commit %2d/%-3d' % (self.commit_upto + 1, + self.commit_count) + + name += target + print line + name, + length = 13 + len(name) + self.ClearLine(length) + + def _GetOutputDir(self, commit_upto): + """Get the name of the output directory for a commit number + + The output directory is typically .../<branch>/<commit>. + + Args: + commit_upto: Commit number to use (0..self.count-1) + """ + commit = self.commits[commit_upto] + subject = commit.subject.translate(trans_valid_chars) + commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1, + self.commit_count, commit.hash, subject[:20])) + output_dir = os.path.join(self.base_dir, commit_dir) + return output_dir + + def GetBuildDir(self, commit_upto, target): + """Get the name of the build directory for a commit number + + The build directory is typically .../<branch>/<commit>/<target>. + + Args: + commit_upto: Commit number to use (0..self.count-1) + target: Target name + """ + output_dir = self._GetOutputDir(commit_upto) + return os.path.join(output_dir, target) + + def GetDoneFile(self, commit_upto, target): + """Get the name of the done file for a commit number + + Args: + commit_upto: Commit number to use (0..self.count-1) + target: Target name + """ + return os.path.join(self.GetBuildDir(commit_upto, target), 'done') + + def GetSizesFile(self, commit_upto, target): + """Get the name of the sizes file for a commit number + + Args: + commit_upto: Commit number to use (0..self.count-1) + target: Target name + """ + return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes') + + def GetFuncSizesFile(self, commit_upto, target, elf_fname): + """Get the name of the funcsizes file for a commit number and ELF file + + Args: + commit_upto: Commit number to use (0..self.count-1) + target: Target name + elf_fname: Filename of elf image + """ + return os.path.join(self.GetBuildDir(commit_upto, target), + '%s.sizes' % elf_fname.replace('/', '-')) + + def GetObjdumpFile(self, commit_upto, target, elf_fname): + """Get the name of the objdump file for a commit number and ELF file + + Args: + commit_upto: Commit number to use (0..self.count-1) + target: Target name + elf_fname: Filename of elf image + """ + return os.path.join(self.GetBuildDir(commit_upto, target), + '%s.objdump' % elf_fname.replace('/', '-')) + + def GetErrFile(self, commit_upto, target): + """Get the name of the err file for a commit number + + Args: + commit_upto: Commit number to use (0..self.count-1) + target: Target name + """ + output_dir = self.GetBuildDir(commit_upto, target) + return os.path.join(output_dir, 'err') + + def FilterErrors(self, lines): + """Filter out errors in which we have no interest + + We should probably use map(). + + Args: + lines: List of error lines, each a string + Returns: + New list with only interesting lines included + """ + out_lines = [] + for line in lines: + if not self.re_make_err.search(line): + out_lines.append(line) + return out_lines + + def ReadFuncSizes(self, fname, fd): + """Read function sizes from the output of 'nm' + + Args: + fd: File containing data to read + fname: Filename we are reading from (just for errors) + + Returns: + Dictionary containing size of each function in bytes, indexed by + function name. + """ + sym = {} + for line in fd.readlines(): + try: + size, type, name = line[:-1].split() + except: + print "Invalid line in file '%s': '%s'" % (fname, line[:-1]) + continue + if type in 'tTdDbB': + # function names begin with '.' on 64-bit powerpc + if '.' in name[1:]: + name = 'static.' + name.split('.')[0] + sym[name] = sym.get(name, 0) + int(size, 16) + return sym + + def GetBuildOutcome(self, commit_upto, target, read_func_sizes): + """Work out the outcome of a build. + + Args: + commit_upto: Commit number to check (0..n-1) + target: Target board to check + read_func_sizes: True to read function size information + + Returns: + Outcome object + """ + done_file = self.GetDoneFile(commit_upto, target) + sizes_file = self.GetSizesFile(commit_upto, target) + sizes = {} + func_sizes = {} + if os.path.exists(done_file): + with open(done_file, 'r') as fd: + return_code = int(fd.readline()) + err_lines = [] + err_file = self.GetErrFile(commit_upto, target) + if os.path.exists(err_file): + with open(err_file, 'r') as fd: + err_lines = self.FilterErrors(fd.readlines()) + + # Decide whether the build was ok, failed or created warnings + if return_code: + rc = OUTCOME_ERROR + elif len(err_lines): + rc = OUTCOME_WARNING + else: + rc = OUTCOME_OK + + # Convert size information to our simple format + if os.path.exists(sizes_file): + with open(sizes_file, 'r') as fd: + for line in fd.readlines(): + values = line.split() + rodata = 0 + if len(values) > 6: + rodata = int(values[6], 16) + size_dict = { + 'all' : int(values[0]) + int(values[1]) + + int(values[2]), + 'text' : int(values[0]) - rodata, + 'data' : int(values[1]), + 'bss' : int(values[2]), + 'rodata' : rodata, + } + sizes[values[5]] = size_dict + + if read_func_sizes: + pattern = self.GetFuncSizesFile(commit_upto, target, '*') + for fname in glob.glob(pattern): + with open(fname, 'r') as fd: + dict_name = os.path.basename(fname).replace('.sizes', + '') + func_sizes[dict_name] = self.ReadFuncSizes(fname, fd) + + return Builder.Outcome(rc, err_lines, sizes, func_sizes) + + return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}) + + def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes): + """Calculate a summary of the results of building a commit. + + Args: + board_selected: Dict containing boards to summarise + commit_upto: Commit number to summarize (0..self.count-1) + read_func_sizes: True to read function size information + + Returns: + Tuple: + Dict containing boards which passed building this commit. + keyed by board.target + List containing a summary of error/warning lines + """ + board_dict = {} + err_lines_summary = [] + + for board in boards_selected.itervalues(): + outcome = self.GetBuildOutcome(commit_upto, board.target, + read_func_sizes) + board_dict[board.target] = outcome + for err in outcome.err_lines: + if err and not err.rstrip() in err_lines_summary: + err_lines_summary.append(err.rstrip()) + return board_dict, err_lines_summary + + def AddOutcome(self, board_dict, arch_list, changes, char, color): + """Add an output to our list of outcomes for each architecture + + This simple function adds failing boards (changes) to the + relevant architecture string, so we can print the results out + sorted by architecture. + + Args: + board_dict: Dict containing all boards + arch_list: Dict keyed by arch name. Value is a string containing + a list of board names which failed for that arch. + changes: List of boards to add to arch_list + color: terminal.Colour object + """ + done_arch = {} + for target in changes: + if target in board_dict: + arch = board_dict[target].arch + else: + arch = 'unknown' + str = self.col.Color(color, ' ' + target) + if not arch in done_arch: + str = self.col.Color(color, char) + ' ' + str + done_arch[arch] = True + if not arch in arch_list: + arch_list[arch] = str + else: + arch_list[arch] += str + + + def ColourNum(self, num): + color = self.col.RED if num > 0 else self.col.GREEN + if num == 0: + return '0' + return self.col.Color(color, str(num)) + + def ResetResultSummary(self, board_selected): + """Reset the results summary ready for use. + + Set up the base board list to be all those selected, and set the + error lines to empty. + + Following this, calls to PrintResultSummary() will use this + information to work out what has changed. + + Args: + board_selected: Dict containing boards to summarise, keyed by + board.target + """ + self._base_board_dict = {} + for board in board_selected: + self._base_board_dict[board] = Builder.Outcome(0, [], [], {}) + self._base_err_lines = [] + + def PrintFuncSizeDetail(self, fname, old, new): + grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0 + delta, common = [], {} + + for a in old: + if a in new: + common[a] = 1 + + for name in old: + if name not in common: + remove += 1 + down += old[name] + delta.append([-old[name], name]) + + for name in new: + if name not in common: + add += 1 + up += new[name] + delta.append([new[name], name]) + + for name in common: + diff = new.get(name, 0) - old.get(name, 0) + if diff > 0: + grow, up = grow + 1, up + diff + elif diff < 0: + shrink, down = shrink + 1, down - diff + delta.append([diff, name]) + + delta.sort() + delta.reverse() + + args = [add, -remove, grow, -shrink, up, -down, up - down] + if max(args) == 0: + return + args = [self.ColourNum(x) for x in args] + indent = ' ' * 15 + print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' % + tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args)) + print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new', + 'delta') + for diff, name in delta: + if diff: + color = self.col.RED if diff > 0 else self.col.GREEN + msg = '%s %-38s %7s %7s %+7d' % (indent, name, + old.get(name, '-'), new.get(name,'-'), diff) + print self.col.Color(color, msg) + + + def PrintSizeDetail(self, target_list, show_bloat): + """Show details size information for each board + + Args: + target_list: List of targets, each a dict containing: + 'target': Target name + 'total_diff': Total difference in bytes across all areas + <part_name>: Difference for that part + show_bloat: Show detail for each function + """ + targets_by_diff = sorted(target_list, reverse=True, + key=lambda x: x['_total_diff']) + for result in targets_by_diff: + printed_target = False + for name in sorted(result): + diff = result[name] + if name.startswith('_'): + continue + if diff != 0: + color = self.col.RED if diff > 0 else self.col.GREEN + msg = ' %s %+d' % (name, diff) + if not printed_target: + print '%10s %-15s:' % ('', result['_target']), + printed_target = True + print self.col.Color(color, msg), + if printed_target: + print + if show_bloat: + target = result['_target'] + outcome = result['_outcome'] + base_outcome = self._base_board_dict[target] + for fname in outcome.func_sizes: + self.PrintFuncSizeDetail(fname, + base_outcome.func_sizes[fname], + outcome.func_sizes[fname]) + + + def PrintSizeSummary(self, board_selected, board_dict, show_detail, + show_bloat): + """Print a summary of image sizes broken down by section. + + The summary takes the form of one line per architecture. The + line contains deltas for each of the sections (+ means the section + got bigger, - means smaller). The nunmbers are the average number + of bytes that a board in this section increased by. + + For example: + powerpc: (622 boards) text -0.0 + arm: (285 boards) text -0.0 + nds32: (3 boards) text -8.0 + + Args: + board_selected: Dict containing boards to summarise, keyed by + board.target + board_dict: Dict containing boards for which we built this + commit, keyed by board.target. The value is an Outcome object. + show_detail: Show detail for each board + show_bloat: Show detail for each function + """ + arch_list = {} + arch_count = {} + + # Calculate changes in size for different image parts + # The previous sizes are in Board.sizes, for each board + for target in board_dict: + if target not in board_selected: + continue + base_sizes = self._base_board_dict[target].sizes + outcome = board_dict[target] + sizes = outcome.sizes + + # Loop through the list of images, creating a dict of size + # changes for each image/part. We end up with something like + # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4} + # which means that U-Boot data increased by 5 bytes and SPL + # text decreased by 4. + err = {'_target' : target} + for image in sizes: + if image in base_sizes: + base_image = base_sizes[image] + # Loop through the text, data, bss parts + for part in sorted(sizes[image]): + diff = sizes[image][part] - base_image[part] + col = None + if diff: + if image == 'u-boot': + name = part + else: + name = image + ':' + part + err[name] = diff + arch = board_selected[target].arch + if not arch in arch_count: + arch_count[arch] = 1 + else: + arch_count[arch] += 1 + if not sizes: + pass # Only add to our list when we have some stats + elif not arch in arch_list: + arch_list[arch] = [err] + else: + arch_list[arch].append(err) + + # We now have a list of image size changes sorted by arch + # Print out a summary of these + for arch, target_list in arch_list.iteritems(): + # Get total difference for each type + totals = {} + for result in target_list: + total = 0 + for name, diff in result.iteritems(): + if name.startswith('_'): + continue + total += diff + if name in totals: + totals[name] += diff + else: + totals[name] = diff + result['_total_diff'] = total + result['_outcome'] = board_dict[result['_target']] + + count = len(target_list) + printed_arch = False + for name in sorted(totals): + diff = totals[name] + if diff: + # Display the average difference in this name for this + # architecture + avg_diff = float(diff) / count + color = self.col.RED if avg_diff > 0 else self.col.GREEN + msg = ' %s %+1.1f' % (name, avg_diff) + if not printed_arch: + print '%10s: (for %d/%d boards)' % (arch, count, + arch_count[arch]), + printed_arch = True + print self.col.Color(color, msg), + + if printed_arch: + print + if show_detail: + self.PrintSizeDetail(target_list, show_bloat) + + + def PrintResultSummary(self, board_selected, board_dict, err_lines, + show_sizes, show_detail, show_bloat): + """Compare results with the base results and display delta. + + Only boards mentioned in board_selected will be considered. This + function is intended to be called repeatedly with the results of + each commit. It therefore shows a 'diff' between what it saw in + the last call and what it sees now. + + Args: + board_selected: Dict containing boards to summarise, keyed by + board.target + board_dict: Dict containing boards for which we built this + commit, keyed by board.target. The value is an Outcome object. + err_lines: A list of errors for this commit, or [] if there is + none, or we don't want to print errors + show_sizes: Show image size deltas + show_detail: Show detail for each board + show_bloat: Show detail for each function + """ + better = [] # List of boards fixed since last commit + worse = [] # List of new broken boards since last commit + new = [] # List of boards that didn't exist last time + unknown = [] # List of boards that were not built + + for target in board_dict: + if target not in board_selected: + continue + + # If the board was built last time, add its outcome to a list + if target in self._base_board_dict: + base_outcome = self._base_board_dict[target].rc + outcome = board_dict[target] + if outcome.rc == OUTCOME_UNKNOWN: + unknown.append(target) + elif outcome.rc < base_outcome: + better.append(target) + elif outcome.rc > base_outcome: + worse.append(target) + else: + new.append(target) + + # Get a list of errors that have appeared, and disappeared + better_err = [] + worse_err = [] + for line in err_lines: + if line not in self._base_err_lines: + worse_err.append('+' + line) + for line in self._base_err_lines: + if line not in err_lines: + better_err.append('-' + line) + + # Display results by arch + if better or worse or unknown or new or worse_err or better_err: + arch_list = {} + self.AddOutcome(board_selected, arch_list, better, '', + self.col.GREEN) + self.AddOutcome(board_selected, arch_list, worse, '+', + self.col.RED) + self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE) + if self._show_unknown: + self.AddOutcome(board_selected, arch_list, unknown, '?', + self.col.MAGENTA) + for arch, target_list in arch_list.iteritems(): + print '%10s: %s' % (arch, target_list) + if better_err: + print self.col.Color(self.col.GREEN, '\n'.join(better_err)) + if worse_err: + print self.col.Color(self.col.RED, '\n'.join(worse_err)) + + if show_sizes: + self.PrintSizeSummary(board_selected, board_dict, show_detail, + show_bloat) + + # Save our updated information for the next call to this function + self._base_board_dict = board_dict + self._base_err_lines = err_lines + + # Get a list of boards that did not get built, if needed + not_built = [] + for board in board_selected: + if not board in board_dict: + not_built.append(board) + if not_built: + print "Boards not built (%d): %s" % (len(not_built), + ', '.join(not_built)) + + + def ShowSummary(self, commits, board_selected, show_errors, show_sizes, + show_detail, show_bloat): + """Show a build summary for U-Boot for a given board list. + + Reset the result summary, then repeatedly call GetResultSummary on + each commit's results, then display the differences we see. + + Args: + commit: Commit objects to summarise + board_selected: Dict containing boards to summarise + show_errors: Show errors that occured + show_sizes: Show size deltas + show_detail: Show detail for each board + show_bloat: Show detail for each function + """ + self.commit_count = len(commits) + self.commits = commits + self.ResetResultSummary(board_selected) + + for commit_upto in range(0, self.commit_count, self._step): + board_dict, err_lines = self.GetResultSummary(board_selected, + commit_upto, read_func_sizes=show_bloat) + msg = '%02d: %s' % (commit_upto + 1, commits[commit_upto].subject) + print self.col.Color(self.col.BLUE, msg) + self.PrintResultSummary(board_selected, board_dict, + err_lines if show_errors else [], show_sizes, show_detail, + show_bloat) + + + def SetupBuild(self, board_selected, commits): + """Set up ready to start a build. + + Args: + board_selected: Selected boards to build + commits: Selected commits to build + """ + # First work out how many commits we will build + count = (len(commits) + self._step - 1) / self._step + self.count = len(board_selected) * count + self.upto = self.warned = self.fail = 0 + self._timestamps = collections.deque() + + def BuildBoardsForCommit(self, board_selected, keep_outputs): + """Build all boards for a single commit""" + self.SetupBuild(board_selected) + self.count = len(board_selected) + for brd in board_selected.itervalues(): + job = BuilderJob() + job.board = brd + job.commits = None + job.keep_outputs = keep_outputs + self.queue.put(brd) + + self.queue.join() + self.out_queue.join() + print + self.ClearLine(0) + + def BuildCommits(self, commits, board_selected, show_errors, keep_outputs): + """Build all boards for all commits (non-incremental)""" + self.commit_count = len(commits) + + self.ResetResultSummary(board_selected) + for self.commit_upto in range(self.commit_count): + self.SelectCommit(commits[self.commit_upto]) + self.SelectOutputDir() + Mkdir(self.output_dir) + + self.BuildBoardsForCommit(board_selected, keep_outputs) + board_dict, err_lines = self.GetResultSummary() + self.PrintResultSummary(board_selected, board_dict, + err_lines if show_errors else []) + + if self.already_done: + print '%d builds already done' % self.already_done + + def GetThreadDir(self, thread_num): + """Get the directory path to the working dir for a thread. + + Args: + thread_num: Number of thread to check. + """ + return os.path.join(self._working_dir, '%02d' % thread_num) + + def _PrepareThread(self, thread_num): + """Prepare the working directory for a thread. + + This clones or fetches the repo into the thread's work directory. + + Args: + thread_num: Thread number (0, 1, ...) + """ + thread_dir = self.GetThreadDir(thread_num) + Mkdir(thread_dir) + git_dir = os.path.join(thread_dir, '.git') + + # Clone the repo if it doesn't already exist + # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so + # we have a private index but uses the origin repo's contents? + if self.git_dir: + src_dir = os.path.abspath(self.git_dir) + if os.path.exists(git_dir): + gitutil.Fetch(git_dir, thread_dir) + else: + print 'Cloning repo for thread %d' % thread_num + gitutil.Clone(src_dir, thread_dir) + + def _PrepareWorkingSpace(self, max_threads): + """Prepare the working directory for use. + + Set up the git repo for each thread. + + Args: + max_threads: Maximum number of threads we expect to need. + """ + Mkdir(self._working_dir) + for thread in range(max_threads): + self._PrepareThread(thread) + + def _PrepareOutputSpace(self): + """Get the output directories ready to receive files. + + We delete any output directories which look like ones we need to + create. Having left over directories is confusing when the user wants + to check the output manually. + """ + dir_list = [] + for commit_upto in range(self.commit_count): + dir_list.append(self._GetOutputDir(commit_upto)) + + for dirname in glob.glob(os.path.join(self.base_dir, '*')): + if dirname not in dir_list: + shutil.rmtree(dirname) + + def BuildBoards(self, commits, board_selected, show_errors, keep_outputs): + """Build all commits for a list of boards + + Args: + commits: List of commits to be build, each a Commit object + boards_selected: Dict of selected boards, key is target name, + value is Board object + show_errors: True to show summarised error/warning info + keep_outputs: True to save build output files + """ + self.commit_count = len(commits) + self.commits = commits + + self.ResetResultSummary(board_selected) + Mkdir(self.base_dir) + self._PrepareWorkingSpace(min(self.num_threads, len(board_selected))) + self._PrepareOutputSpace() + self.SetupBuild(board_selected, commits) + self.ProcessResult(None) + + # Create jobs to build all commits for each board + for brd in board_selected.itervalues(): + job = BuilderJob() + job.board = brd + job.commits = commits + job.keep_outputs = keep_outputs + job.step = self._step + self.queue.put(job) + + # Wait until all jobs are started + self.queue.join() + + # Wait until we have processed all output + self.out_queue.join() + print + self.ClearLine(0) diff --git a/tools/buildman/buildman b/tools/buildman/buildman new file mode 120000 index 0000000..e4fba2d --- /dev/null +++ b/tools/buildman/buildman @@ -0,0 +1 @@ +buildman.py
\ No newline at end of file diff --git a/tools/buildman/buildman.py b/tools/buildman/buildman.py new file mode 100755 index 0000000..7b05d0f --- /dev/null +++ b/tools/buildman/buildman.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# +# Copyright (c) 2012 The Chromium OS Authors. +# +# See file CREDITS for list of people who contributed to this +# project. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# + +"""See README for more information""" + +import multiprocessing +from optparse import OptionParser +import os +import re +import sys +import unittest + +# Bring in the patman libraries +our_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(our_path, '../patman')) + +# Our modules +import board +import builder +import checkpatch +import command +import control +import doctest +import gitutil +import patchstream +import terminal +import toolchain + +def RunTests(): + import test + + sys.argv = [sys.argv[0]] + suite = unittest.TestLoader().loadTestsFromTestCase(test.TestBuild) + result = unittest.TestResult() + suite.run(result) + + # TODO: Surely we can just 'print' result? + print result + for test, err in result.errors: + print err + for test, err in result.failures: + print err + + +parser = OptionParser() +parser.add_option('-b', '--branch', type='string', + help='Branch name to build') +parser.add_option('-B', '--bloat', dest='show_bloat', + action='store_true', default=False, + help='Show changes in function code size for each board') +parser.add_option('-c', '--count', dest='count', type='int', + default=-1, help='Run build on the top n commits') +parser.add_option('-e', '--show_errors', action='store_true', + default=False, help='Show errors and warnings') +parser.add_option('-f', '--force-build', dest='force_build', + action='store_true', default=False, + help='Force build of boards even if already built') +parser.add_option('-d', '--detail', dest='show_detail', + action='store_true', default=False, + help='Show detailed information for each board in summary') +parser.add_option('-g', '--git', type='string', + help='Git repo containing branch to build', default='.') +parser.add_option('-H', '--full-help', action='store_true', dest='full_help', + default=False, help='Display the README file') +parser.add_option('-j', '--jobs', dest='jobs', type='int', + default=None, help='Number of jobs to run at once (passed to make)') +parser.add_option('-k', '--keep-outputs', action='store_true', + default=False, help='Keep all build output files (e.g. binaries)') +parser.add_option('--list-tool-chains', action='store_true', default=False, + help='List available tool chains') +parser.add_option('-n', '--dry-run', action='store_true', dest='dry_run', + default=False, help="Do a try run (describe actions, but no nothing)") +parser.add_option('-Q', '--quick', action='store_true', + default=False, help='Do a rough build, with limited warning resolution') +parser.add_option('-s', '--summary', action='store_true', + default=False, help='Show a build summary') +parser.add_option('-S', '--show-sizes', action='store_true', + default=False, help='Show image size variation in summary') +parser.add_option('--step', type='int', + default=1, help='Only build every n commits (0=just first and last)') +parser.add_option('-t', '--test', action='store_true', dest='test', + default=False, help='run tests') +parser.add_option('-T', '--threads', type='int', + default=None, help='Number of builder threads to use') +parser.add_option('-u', '--show_unknown', action='store_true', + default=False, help='Show boards with unknown build result') + +parser.usage = """buildman -b <branch> [options] + +Build U-Boot for all commits in a branch. Use -n to do a dry run""" + +(options, args) = parser.parse_args() + +# Run our meagre tests +if options.test: + RunTests() +elif options.full_help: + pager = os.getenv('PAGER') + if not pager: + pager = 'more' + fname = os.path.join(os.path.dirname(sys.argv[0]), 'README') + command.Run(pager, fname) + +# Build selected commits for selected boards +else: + control.DoBuildman(options, args) diff --git a/tools/buildman/control.py b/tools/buildman/control.py new file mode 100644 index 0000000..8d7b9b5 --- /dev/null +++ b/tools/buildman/control.py @@ -0,0 +1,181 @@ +# Copyright (c) 2013 The Chromium OS Authors. +# +# See file CREDITS for list of people who contributed to this +# project. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# + +import multiprocessing +import os +import sys + +import board +import bsettings +from builder import Builder +import gitutil +import patchstream +import terminal +import toolchain + +def GetPlural(count): + """Returns a plural 's' if count is not 1""" + return 's' if count != 1 else '' + +def GetActionSummary(is_summary, count, selected, options): + """Return a string summarising the intended action. + + Returns: + Summary string. + """ + count = (count + options.step - 1) / options.step + str = '%s %d commit%s for %d boards' % ( + 'Summary of' if is_summary else 'Building', count, GetPlural(count), + len(selected)) + str += ' (%d thread%s, %d job%s per thread)' % (options.threads, + GetPlural(options.threads), options.jobs, GetPlural(options.jobs)) + return str + +def ShowActions(series, why_selected, boards_selected, builder, options): + """Display a list of actions that we would take, if not a dry run. + + Args: + series: Series object + why_selected: Dictionary where each key is a buildman argument + provided by the user, and the value is the boards brought + in by that argument. For example, 'arm' might bring in + 400 boards, so in this case the key would be 'arm' and + the value would be a list of board names. + boards_selected: Dict of selected boards, key is target name, + value is Board object + builder: The builder that will be used to build the commits + options: Command line options object + """ + col = terminal.Color() + print 'Dry run, so not doing much. But I would do this:' + print + print GetActionSummary(False, len(series.commits), boards_selected, + options) + print 'Build directory: %s' % builder.base_dir + for upto in range(0, len(series.commits), options.step): + commit = series.commits[upto] + print ' ', col.Color(col.YELLOW, commit.hash, bright=False), + print commit.subject + print + for arg in why_selected: + if arg != 'all': + print arg, ': %d boards' % why_selected[arg] + print ('Total boards to build for each commit: %d\n' % + why_selected['all']) + +def DoBuildman(options, args): + """The main control code for buildman + + Args: + options: Command line options object + args: Command line arguments (list of strings) + """ + gitutil.Setup() + + bsettings.Setup() + options.git_dir = os.path.join(options.git, '.git') + + toolchains = toolchain.Toolchains() + toolchains.Scan(options.list_tool_chains) + if options.list_tool_chains: + toolchains.List() + print + return + + # Work out how many commits to build. We want to build everything on the + # branch. We also build the upstream commit as a control so we can see + # problems introduced by the first commit on the branch. + col = terminal.Color() + count = options.count + if count == -1: + if not options.branch: + str = 'Please use -b to specify a branch to build' + print col.Color(col.RED, str) + sys.exit(1) + count = gitutil.CountCommitsInBranch(options.git_dir, options.branch) + count += 1 # Build upstream commit also + + if not count: + str = ("No commits found to process in branch '%s': " + "set branch's upstream or use -c flag" % options.branch) + print col.Color(col.RED, str) + sys.exit(1) + + # Work out what subset of the boards we are building + boards = board.Boards() + boards.ReadBoards(os.path.join(options.git, 'boards.cfg')) + why_selected = boards.SelectBoards(args) + selected = boards.GetSelected() + if not len(selected): + print col.Color(col.RED, 'No matching boards found') + sys.exit(1) + + # Read the metadata from the commits. First look at the upstream commit, + # then the ones in the branch. We would like to do something like + # upstream/master~..branch but that isn't possible if upstream/master is + # a merge commit (it will list all the commits that form part of the + # merge) + range_expr = gitutil.GetRangeInBranch(options.git_dir, options.branch) + upstream_commit = gitutil.GetUpstream(options.git_dir, options.branch) + series = patchstream.GetMetaDataForList(upstream_commit, options.git_dir, + 1) + series = patchstream.GetMetaDataForList(range_expr, options.git_dir, None, + series) + + # By default we have one thread per CPU. But if there are not enough jobs + # we can have fewer threads and use a high '-j' value for make. + if not options.threads: + options.threads = min(multiprocessing.cpu_count(), len(selected)) + if not options.jobs: + options.jobs = max(1, (multiprocessing.cpu_count() + + len(selected) - 1) / len(selected)) + + if not options.step: + options.step = len(series.commits) - 1 + + # Create a new builder with the selected options + output_dir = os.path.join('..', options.branch) + builder = Builder(toolchains, output_dir, options.git_dir, + options.threads, options.jobs, checkout=True, + show_unknown=options.show_unknown, step=options.step) + builder.force_config_on_failure = not options.quick + + # For a dry run, just show our actions as a sanity check + if options.dry_run: + ShowActions(series, why_selected, selected, builder, options) + else: + builder.force_build = options.force_build + + # Work out which boards to build + board_selected = boards.GetSelectedDict() + + print GetActionSummary(options.summary, count, board_selected, options) + + if options.summary: + # We can't show function sizes without board details at present + if options.show_bloat: + options.show_detail = True + builder.ShowSummary(series.commits, board_selected, + options.show_errors, options.show_sizes, + options.show_detail, options.show_bloat) + else: + builder.BuildBoards(series.commits, board_selected, + options.show_errors, options.keep_outputs) diff --git a/tools/buildman/test.py b/tools/buildman/test.py new file mode 100644 index 0000000..9330fa5 --- /dev/null +++ b/tools/buildman/test.py @@ -0,0 +1,185 @@ +# +# Copyright (c) 2012 The Chromium OS Authors. +# +# See file CREDITS for list of people who contributed to this +# project. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# + +import os +import shutil +import sys +import tempfile +import time +import unittest + +# Bring in the patman libraries +our_path = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(our_path, '../patman')) + +import board +import bsettings +import builder +import control +import command +import commit +import toolchain + +errors = [ + '''main.c: In function 'main_loop': +main.c:260:6: warning: unused variable 'joe' [-Wunused-variable] +''', + '''main.c: In function 'main_loop': +main.c:295:2: error: 'fred' undeclared (first use in this function) +main.c:295:2: note: each undeclared identifier is reported only once for each function it appears in +make[1]: *** [main.o] Error 1 +make: *** [common/libcommon.o] Error 2 +Make failed +''', + '''main.c: In function 'main_loop': +main.c:280:6: warning: unused variable 'mary' [-Wunused-variable] +''', + '''powerpc-linux-ld: warning: dot moved backwards before `.bss' +powerpc-linux-ld: warning: dot moved backwards before `.bss' +powerpc-linux-ld: u-boot: section .text lma 0xfffc0000 overlaps previous sections +powerpc-linux-ld: u-boot: section .rodata lma 0xfffef3ec overlaps previous sections +powerpc-linux-ld: u-boot: section .reloc lma 0xffffa400 overlaps previous sections +powerpc-linux-ld: u-boot: section .data lma 0xffffcd38 overlaps previous sections +powerpc-linux-ld: u-boot: section .u_boot_cmd lma 0xffffeb40 overlaps previous sections +powerpc-linux-ld: u-boot: section .bootpg lma 0xfffff198 overlaps previous sections +''' +] + + +# hash, subject, return code, list of errors/warnings +commits = [ + ['1234', 'upstream/master, ok', 0, []], + ['5678', 'Second commit, a warning', 0, errors[0:1]], + ['9012', 'Third commit, error', 1, errors[0:2]], + ['3456', 'Fourth commit, warning', 0, [errors[0], errors[2]]], + ['7890', 'Fifth commit, link errors', 1, [errors[0], errors[3]]], + ['abcd', 'Sixth commit, fixes all errors', 0, []] +] + +boards = [ + ['board0', 'arm', 'armv7', 'ARM Board 1', 'Tester', '', ''], + ['board1', 'arm', 'armv7', 'ARM Board 2', 'Tester', '', ''], + ['board2', 'powerpc', 'powerpc', 'PowerPC board 1', 'Tester', '', ''], + ['board3', 'powerpc', 'mpc5xx', 'PowerPC board 2', 'Tester', '', ''], + ['board4', 'sandbox', 'sandbox', 'Sandbox board', 'Tester', '', ''] +] + +class Options: + """Class that holds build options""" + pass + +class TestBuild(unittest.TestCase): + """Test buildman + + TODO: Write tests for the rest of the functionality + """ + def setUp(self): + # Set up commits to build + self.commits = [] + sequence = 0 + for commit_info in commits: + comm = commit.Commit(commit_info[0]) + comm.subject = commit_info[1] + comm.return_code = commit_info[2] + comm.error_list = commit_info[3] + comm.sequence = sequence + sequence += 1 + self.commits.append(comm) + + # Set up boards to build + self.boards = board.Boards() + for brd in boards: + self.boards.AddBoard(board.Board(*brd)) + self.boards.SelectBoards([]) + + # Set up the toolchains + bsettings.Setup() + self.toolchains = toolchain.Toolchains() + self.toolchains.Add('arm-linux-gcc', test=False) + self.toolchains.Add('sparc-linux-gcc', test=False) + self.toolchains.Add('powerpc-linux-gcc', test=False) + self.toolchains.Add('gcc', test=False) + + def Make(self, commit, brd, stage, *args, **kwargs): + result = command.CommandResult() + boardnum = int(brd.target[-1]) + result.return_code = 0 + result.stderr = '' + result.stdout = ('This is the test output for board %s, commit %s' % + (brd.target, commit.hash)) + if boardnum >= 1 and boardnum >= commit.sequence: + result.return_code = commit.return_code + result.stderr = ''.join(commit.error_list) + if stage == 'build': + target_dir = None + for arg in args: + if arg.startswith('O='): + target_dir = arg[2:] + + if not os.path.isdir(target_dir): + os.mkdir(target_dir) + #time.sleep(.2 + boardnum * .2) + + result.combined = result.stdout + result.stderr + return result + + def testBasic(self): + """Test basic builder operation""" + output_dir = tempfile.mkdtemp() + if not os.path.isdir(output_dir): + os.mkdir(output_dir) + build = builder.Builder(self.toolchains, output_dir, None, 1, 2, + checkout=False, show_unknown=False) + build.do_make = self.Make + board_selected = self.boards.GetSelectedDict() + + #build.BuildCommits(self.commits, board_selected, False) + build.BuildBoards(self.commits, board_selected, False, False) + build.ShowSummary(self.commits, board_selected, True, False, + False, False) + + def _testGit(self): + """Test basic builder operation by building a branch""" + base_dir = tempfile.mkdtemp() + if not os.path.isdir(base_dir): + os.mkdir(base_dir) + options = Options() + options.git = os.getcwd() + options.summary = False + options.jobs = None + options.dry_run = False + #options.git = os.path.join(base_dir, 'repo') + options.branch = 'test-buildman' + options.force_build = False + options.list_tool_chains = False + options.count = -1 + options.git_dir = None + options.threads = None + options.show_unknown = False + options.quick = False + options.show_errors = False + options.keep_outputs = False + args = ['tegra20'] + control.DoBuildman(options, args) + +if __name__ == "__main__": + unittest.main() diff --git a/tools/buildman/toolchain.py b/tools/buildman/toolchain.py new file mode 100644 index 0000000..e0a6970 --- /dev/null +++ b/tools/buildman/toolchain.py @@ -0,0 +1,185 @@ +# Copyright (c) 2012 The Chromium OS Authors. +# +# See file CREDITS for list of people who contributed to this +# project. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307 USA +# + +import glob +import os + +import bsettings +import command + +class Toolchain: + """A single toolchain + + Public members: + gcc: Full path to C compiler + path: Directory path containing C compiler + cross: Cross compile string, e.g. 'arm-linux-' + arch: Architecture of toolchain as determined from the first + component of the filename. E.g. arm-linux-gcc becomes arm + """ + + def __init__(self, fname, test, verbose=False): + """Create a new toolchain object. + + Args: + fname: Filename of the gcc component + test: True to run the toolchain to test it + """ + self.gcc = fname + self.path = os.path.dirname(fname) + self.cross = os.path.basename(fname)[:-3] + pos = self.cross.find('-') + self.arch = self.cross[:pos] if pos != -1 else 'sandbox' + + env = self.MakeEnvironment() + + # As a basic sanity check, run the C compiler with --version + cmd = [fname, '--version'] + if test: + result = command.RunPipe([cmd], capture=True, env=env) + self.ok = result.return_code == 0 + if verbose: + print 'Tool chain test: ', + if self.ok: + print 'OK' + else: + print 'BAD' + print 'Command: ', cmd + print result.stdout + print result.stderr + else: + self.ok = True + self.priority = self.GetPriority(fname) + + def GetPriority(self, fname): + """Return the priority of the toolchain. + + Toolchains are ranked according to their suitability by their + filename prefix. + + Args: + fname: Filename of toolchain + Returns: + Priority of toolchain, 0=highest, 20=lowest. + """ + priority_list = ['-elf', '-unknown-linux-gnu', '-linux', '-elf', + '-none-linux-gnueabi', '-uclinux', '-none-eabi', + '-gentoo-linux-gnu', '-linux-gnueabi', '-le-linux', '-uclinux'] + for prio in range(len(priority_list)): + if priority_list[prio] in fname: + return prio + return prio + + def MakeEnvironment(self): + """Returns an environment for using the toolchain. + + Thie takes the current environment, adds CROSS_COMPILE and + augments PATH so that the toolchain will operate correctly. + """ + env = dict(os.environ) + env['CROSS_COMPILE'] = self.cross + env['PATH'] += (':' + self.path) + return env + + +class Toolchains: + """Manage a list of toolchains for building U-Boot + + We select one toolchain for each architecture type + + Public members: + toolchains: Dict of Toolchain objects, keyed by architecture name + paths: List of paths to check for toolchains (may contain wildcards) + """ + + def __init__(self): + self.toolchains = {} + self.paths = [] + for name, value in bsettings.GetItems('toolchain'): + if '*' in value: + self.paths += glob.glob(value) + else: + self.paths.append(value) + + + def Add(self, fname, test=True, verbose=False): + """Add a toolchain to our list + + We select the given toolchain as our preferred one for its + architecture if it is a higher priority than the others. + + Args: + fname: Filename of toolchain's gcc driver + test: True to run the toolchain to test it + """ + toolchain = Toolchain(fname, test, verbose) + add_it = toolchain.ok + if toolchain.arch in self.toolchains: + add_it = (toolchain.priority < + self.toolchains[toolchain.arch].priority) + if add_it: + self.toolchains[toolchain.arch] = toolchain + + def Scan(self, verbose): + """Scan for available toolchains and select the best for each arch. + + We look for all the toolchains we can file, figure out the + architecture for each, and whether it works. Then we select the + highest priority toolchain for each arch. + + Args: + verbose: True to print out progress information + """ + if verbose: print 'Scanning for tool chains' + for path in self.paths: + if verbose: print " - scanning path '%s'" % path + for subdir in ['.', 'bin', 'usr/bin']: + dirname = os.path.join(path, subdir) + if verbose: print " - looking in '%s'" % dirname + for fname in glob.glob(dirname + '/*gcc'): + if verbose: print " - found '%s'" % fname + self.Add(fname, True, verbose) + + def List(self): + """List out the selected toolchains for each architecture""" + print 'List of available toolchains (%d):' % len(self.toolchains) + if len(self.toolchains): + for key, value in sorted(self.toolchains.iteritems()): + print '%-10s: %s' % (key, value.gcc) + else: + print 'None' + + def Select(self, arch): + """Returns the toolchain for a given architecture + + Args: + args: Name of architecture (e.g. 'arm', 'ppc_8xx') + + returns: + toolchain object, or None if none found + """ + for name, value in bsettings.GetItems('toolchain-alias'): + if arch == name: + arch = value + + if not arch in self.toolchains: + raise ValueError, ("No tool chain found for arch '%s'" % arch) + return self.toolchains[arch] |