]> git.ipfire.org Git - people/ms/u-boot.git/commitdiff
Merge branch 'buildman' of git://git.denx.de/u-boot-x86
authorTom Rini <trini@ti.com>
Thu, 15 Jan 2015 15:18:05 +0000 (10:18 -0500)
committerTom Rini <trini@ti.com>
Thu, 15 Jan 2015 15:18:05 +0000 (10:18 -0500)
tools/buildman/README
tools/buildman/bsettings.py
tools/buildman/builder.py
tools/buildman/builderthread.py
tools/buildman/cmdline.py
tools/buildman/control.py
tools/buildman/test.py
tools/buildman/toolchain.py
tools/patman/gitutil.py

index 0f8ea200f54980b9f840b3df5b4206a6cd8193ca..cf7bf5c17e15025a767a991946ebd98822203e4b 100644 (file)
@@ -141,8 +141,8 @@ $ 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:
+2. Create ~/.buildman to tell buildman where to find tool chains (see 'The
+.buildman file' later for details). As an example:
 
 # Buildman settings file
 
@@ -171,7 +171,16 @@ The toolchain-alias section indicates that the i386 toolchain should be used
 to build x86 commits.
 
 
-2. Check the available toolchains
+3. Make sure you have the require Python pre-requisites
+
+Buildman uses multiprocessing, Queue, shutil, StringIO, ConfigParser and
+urllib2. These should normally be available, but if you get an error like
+this then you will need to obtain those modules:
+
+    ImportError: No module named multiprocessing
+
+
+4. Check the available toolchains
 
 Run this check to make sure that you have a toolchain for every architecture.
 
@@ -301,6 +310,47 @@ You can see that everything is covered, even some strange ones that won't
 be used (c88 and c99). This is a feature.
 
 
+5. Install new toolchains if needed
+
+You can download toolchains and update the [toolchain] section of the
+settings file to find them.
+
+To make this easier, buildman can automatically download and install
+toolchains from kernel.org. First list the available architectures:
+
+$ ./tools/buildman/buildman sandbox --fetch-arch list
+Checking: https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.6.3/
+Checking: https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.6.2/
+Checking: https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.5.1/
+Checking: https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.2.4/
+Available architectures: alpha am33_2.0 arm avr32 bfin cris crisv32 frv h8300
+hppa hppa64 i386 ia64 m32r m68k mips mips64 or32 powerpc powerpc64 s390x sh4
+sparc sparc64 tilegx x86_64 xtensa
+
+Then pick one and download it:
+
+$ ./tools/buildman/buildman sandbox --fetch-arch or32
+Checking: https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.6.3/
+Checking: https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.6.2/
+Checking: https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.5.1/
+Downloading: https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.5.1//x86_64-gcc-4.5.1-nolibc_or32-linux.tar.xz
+Unpacking to: /home/sjg/.buildman-toolchains
+Testing
+      - looking in '/home/sjg/.buildman-toolchains/gcc-4.5.1-nolibc/or32-linux/.'
+      - looking in '/home/sjg/.buildman-toolchains/gcc-4.5.1-nolibc/or32-linux/bin'
+         - found '/home/sjg/.buildman-toolchains/gcc-4.5.1-nolibc/or32-linux/bin/or32-linux-gcc'
+Tool chain test:  OK
+
+Buildman should now be set up to use your new toolchain.
+
+At the time of writing, U-Boot has these architectures:
+
+   arc, arm, avr32, blackfin, m68k, microblaze, mips, nds32, nios2, openrisc
+   powerpc, sandbox, sh, sparc, x86
+
+Of these, only arc, microblaze and nds32 are not available at kernel.org..
+
+
 How to run it
 =============
 
@@ -310,8 +360,9 @@ 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.
+doing something like 'git branch --set-upstream-to upstream/master'
+or something similar. Buildman will try to guess a suitable upstream branch
+if it can't find one (you will see a message like" Guessing upstream as ...).
 
 As an example:
 
@@ -665,28 +716,62 @@ It is common when refactoring code for the rodata to decrease as the text size
 increases, and vice versa.
 
 
-Providing 'make' flags
-======================
+The .buildman file
+==================
+
+The .buildman file provides information about the available toolchains and
+also allows build flags to be passed to 'make'. It consists of several
+sections, with the section name in square brackets. Within each section are
+a set of (tag, value) pairs.
+
+'[toolchain]' section
+
+    This lists the available toolchains. The tag here doesn't matter, but
+    make sure it is unique. The value is the path to the toolchain. Buildman
+    will look in that path for a file ending in 'gcc'. It will then execute
+    it to check that it is a C compiler, passing only the --version flag to
+    it. If the return code is 0, buildman assumes that it is a valid C
+    compiler. It uses the first part of the name as the architecture and
+    strips off the last part when setting the CROSS_COMPILE environment
+    variable (parts are delimited with a hyphen).
+
+    For example powerpc-linux-gcc will be noted as a toolchain for 'powerpc'
+    and CROSS_COMPILE will be set to powerpc-linux- when using it.
+
+'[toolchain-alias]' section
+
+    This converts toolchain architecture names to U-Boot names. For example,
+    if an x86 toolchains is called i386-linux-gcc it will not normally be
+    used for architecture 'x86'. Adding 'x86: i386 x86_64' to this section
+    will tell buildman that the i386 and x86_64 toolchains can be used for
+    the x86 architecture.
+
+'[make-flags]' section
 
-U-Boot's build system supports a few flags (such as BUILD_TAG) which affect
-the build product. These flags can be specified in the buildman settings
-file. They can also be useful when building U-Boot against other open source
-software.
+    U-Boot's build system supports a few flags (such as BUILD_TAG) which
+    affect the build product. These flags can be specified in the buildman
+    settings file. They can also be useful when building U-Boot against other
+    open source software.
 
-[make-flags]
-at91-boards=ENABLE_AT91_TEST=1
-snapper9260=${at91-boards} BUILD_TAG=442
-snapper9g45=${at91-boards} BUILD_TAG=443
+    [make-flags]
+    at91-boards=ENABLE_AT91_TEST=1
+    snapper9260=${at91-boards} BUILD_TAG=442
+    snapper9g45=${at91-boards} BUILD_TAG=443
 
-This will use 'make ENABLE_AT91_TEST=1 BUILD_TAG=442' for snapper9260
-and 'make ENABLE_AT91_TEST=1 BUILD_TAG=443' for snapper9g45. A special
-variable ${target} is available to access the target name (snapper9260 and
-snapper9g20 in this case). Variables are resolved recursively. Note that
-variables can only contain the characters A-Z, a-z, 0-9, hyphen (-) and
-underscore (_).
+    This will use 'make ENABLE_AT91_TEST=1 BUILD_TAG=442' for snapper9260
+    and 'make ENABLE_AT91_TEST=1 BUILD_TAG=443' for snapper9g45. A special
+    variable ${target} is available to access the target name (snapper9260
+    and snapper9g20 in this case). Variables are resolved recursively. Note
+    that variables can only contain the characters A-Z, a-z, 0-9, hyphen (-)
+    and underscore (_).
 
-It is expected that any variables added are dealt with in U-Boot's
-config.mk file and documented in the README.
+    It is expected that any variables added are dealt with in U-Boot's
+    config.mk file and documented in the README.
+
+    Note that you can pass ad-hoc options to the build using environment
+    variables, for example:
+
+       SOME_OPTION=1234 ./tools/buildman/buildman my_board
 
 
 Quick Sanity Check
@@ -698,6 +783,17 @@ build the selected boards and display build status as it runs (i.e. -v is
 enabled automatically). Use -e to see errors/warnings as well.
 
 
+Building Ranges
+===============
+
+You can build a range of commits by specifying a range instead of a branch
+when using the -b flag. For example:
+
+    upstream/master..us-buildman
+
+will build commits in us-buildman that are not in upstream/master.
+
+
 Other options
 =============
 
index fdd875b0736f9ec918c443b50778c84aef33cdfa..b36146918005b92ce6eafd5af88ba664b0200bfa 100644 (file)
@@ -40,7 +40,16 @@ def GetItems(section):
     try:
         return settings.items(section)
     except ConfigParser.NoSectionError as e:
-        print e
         return []
     except:
         raise
+
+def SetItem(section, tag, value):
+    """Set an item and write it back to the settings file"""
+    global settings
+    global config_fname
+
+    settings.set(section, tag, value)
+    if config_fname is not None:
+        with open(config_fname, 'w') as fd:
+            settings.write(fd)
index 7002034221d12789473d33e7f62cb21cbe296be7..1b0ad9927592d9b22ca63893de0bd2255db8fb7d 100644 (file)
@@ -174,7 +174,8 @@ class Builder:
             self.func_sizes = func_sizes
 
     def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
-                 gnu_make='make', checkout=True, show_unknown=True, step=1):
+                 gnu_make='make', checkout=True, show_unknown=True, step=1,
+                 no_subdirs=False, full_path=False, verbose_build=False):
         """Create a new Builder object
 
         Args:
@@ -188,6 +189,11 @@ class Builder:
                 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
+            no_subdirs: Don't create subdirectories when building current
+                source for a single board
+            full_path: Return the full path in CROSS_COMPILE and don't set
+                PATH
+            verbose_build: Run build with V=1 and don't use 'make -s'
         """
         self.toolchains = toolchains
         self.base_dir = base_dir
@@ -213,6 +219,9 @@ class Builder:
         self._step = step
         self.in_tree = False
         self._error_lines = 0
+        self.no_subdirs = no_subdirs
+        self.full_path = full_path
+        self.verbose_build = verbose_build
 
         self.col = terminal.Color()
 
@@ -392,15 +401,17 @@ class Builder:
         Args:
             commit_upto: Commit number to use (0..self.count-1)
         """
+        commit_dir = None
         if self.commits:
             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]))
-        else:
+        elif not self.no_subdirs:
             commit_dir = 'current'
-        output_dir = os.path.join(self.base_dir, commit_dir)
-        return output_dir
+        if not commit_dir:
+            return self.base_dir
+        return os.path.join(self.base_dir, commit_dir)
 
     def GetBuildDir(self, commit_upto, target):
         """Get the name of the build directory for a commit number
@@ -1115,6 +1126,8 @@ class Builder:
         create. Having left over directories is confusing when the user wants
         to check the output manually.
         """
+        if not self.commits:
+            return
         dir_list = []
         for commit_upto in range(self.commit_count):
             dir_list.append(self._GetOutputDir(commit_upto))
index bc4541cb3eb90b357e4608aca5ceae583eee1c05..efb62f16d7020793d6f24439fd34c46df05134db 100644 (file)
@@ -177,7 +177,7 @@ class BuilderThread(threading.Thread):
                     commit = 'current'
 
                 # Set up the environment and command line
-                env = self.toolchain.MakeEnvironment()
+                env = self.toolchain.MakeEnvironment(self.builder.full_path)
                 Mkdir(out_dir)
                 args = []
                 cwd = work_dir
@@ -197,7 +197,8 @@ class BuilderThread(threading.Thread):
                         src_dir = os.getcwd()
                     else:
                         args.append('O=build')
-                args.append('-s')
+                if not self.builder.verbose_build:
+                    args.append('-s')
                 if self.builder.num_jobs is not None:
                     args.extend(['-j', str(self.builder.num_jobs)])
                 config_args = ['%s_defconfig' % brd.target]
@@ -284,7 +285,7 @@ class BuilderThread(threading.Thread):
                 print >>fd, 'path', result.toolchain.path
 
             # Write out the image and function size information and an objdump
-            env = result.toolchain.MakeEnvironment()
+            env = result.toolchain.MakeEnvironment(self.builder.full_path)
             lines = []
             for fname in ['u-boot', 'spl/u-boot-spl']:
                 cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
index 27d3c708e6f311d473ed8f6cb9200d82ba148779..e8a6dadd1c7dfd8cca279b7c5cde03de337315f9 100644 (file)
@@ -36,6 +36,10 @@ def ParseArgs():
     parser.add_option('-F', '--force-build-failures', dest='force_build_failures',
           action='store_true', default=False,
           help='Force build of previously-failed build')
+    parser.add_option('--fetch-arch', type='string',
+          help="Fetch a toolchain for architecture FETCH_ARCH ('list' to list)."
+              ' You can also fetch several toolchains separate by comma, or'
+              " 'all' to download all")
     parser.add_option('-g', '--git', type='string',
           help='Git repo containing branch to build', default='.')
     parser.add_option('-G', '--config-file', type='string',
@@ -55,11 +59,15 @@ def ParseArgs():
           help='List available tool chains')
     parser.add_option('-n', '--dry-run', action='store_true', dest='dry_run',
           default=False, help="Do a dry run (describe actions, but do nothing)")
+    parser.add_option('-N', '--no-subdirs', action='store_true', dest='no_subdirs',
+          default=False, help="Don't create subdirectories when building current source for a single board")
     parser.add_option('-o', '--output-dir', type='string',
           dest='output_dir', default='..',
           help='Directory where all builds happen and buildman has its workspace (default is ../)')
     parser.add_option('-Q', '--quick', action='store_true',
           default=False, help='Do a rough build, with limited warning resolution')
+    parser.add_option('-p', '--full-path', action='store_true',
+          default=False, help="Use full toolchain path in CROSS_COMPILE")
     parser.add_option('-s', '--summary', action='store_true',
           default=False, help='Show a build summary')
     parser.add_option('-S', '--show-sizes', action='store_true',
@@ -74,6 +82,8 @@ def ParseArgs():
           default=False, help='Show boards with unknown build result')
     parser.add_option('-v', '--verbose', action='store_true',
           default=False, help='Show build results while the build progresses')
+    parser.add_option('-V', '--verbose-build', action='store_true',
+          default=False, help='Run make with V=1, showing all output')
     parser.add_option('-x', '--exclude', dest='exclude',
           type='string', action='append',
           help='Specify a list of boards to exclude, separated by comma')
index fb79a1ecfef19c8087998eb5b95d1a196a4d50e2..720b978b238ffb8d664f03278b6a7f29cb7f3d73 100644 (file)
@@ -118,21 +118,45 @@ def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
         print
         return 0
 
+    if options.fetch_arch:
+        if options.fetch_arch == 'list':
+            sorted_list = toolchains.ListArchs()
+            print 'Available architectures: %s\n' % ' '.join(sorted_list)
+            return 0
+        else:
+            fetch_arch = options.fetch_arch
+            if fetch_arch == 'all':
+                fetch_arch = ','.join(toolchains.ListArchs())
+                print 'Downloading toolchains: %s\n' % fetch_arch
+            for arch in fetch_arch.split(','):
+                ret = toolchains.FetchAndInstall(arch)
+                if ret:
+                    return ret
+            return 0
+
     # 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
+    has_range = options.branch and '..' in options.branch
     if count == -1:
         if not options.branch:
             count = 1
         else:
-            count = gitutil.CountCommitsInBranch(options.git_dir,
-                                                 options.branch)
+            if has_range:
+                count, msg = gitutil.CountCommitsInRange(options.git_dir,
+                                                         options.branch)
+            else:
+                count, msg = gitutil.CountCommitsInBranch(options.git_dir,
+                                                          options.branch)
             if count is None:
-                str = ("Branch '%s' not found or has no upstream" %
-                       options.branch)
-                sys.exit(col.Color(col.RED, str))
+                sys.exit(col.Color(col.RED, msg))
+            elif count == 0:
+                sys.exit(col.Color(col.RED, "Range '%s' has no commits" %
+                                   options.branch))
+            if msg:
+                print col.Color(col.YELLOW, msg)
             count += 1   # Build upstream commit also
 
     if not count:
@@ -172,8 +196,11 @@ def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
     # to overwrite earlier ones by setting allow_overwrite=True
     if options.branch:
         if count == -1:
-            range_expr = gitutil.GetRangeInBranch(options.git_dir,
-                                                  options.branch)
+            if has_range:
+                range_expr = options.branch
+            else:
+                range_expr = gitutil.GetRangeInBranch(options.git_dir,
+                                                      options.branch)
             upstream_commit = gitutil.GetUpstream(options.git_dir,
                                                   options.branch)
             series = patchstream.GetMetaDataForList(upstream_commit,
@@ -207,17 +234,22 @@ def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
     if not gnu_make:
         sys.exit('GNU Make not found')
 
-    # Create a new builder with the selected options
+    # Create a new builder with the selected options.
+    output_dir = options.output_dir
     if options.branch:
         dirname = options.branch.replace('/', '_')
-    else:
-        dirname = 'current'
-    output_dir = os.path.join(options.output_dir, dirname)
-    if clean_dir and os.path.exists(output_dir):
+        # As a special case allow the board directory to be placed in the
+        # output directory itself rather than any subdirectory.
+        if not options.no_subdirs:
+            output_dir = os.path.join(options.output_dir, dirname)
+    if (clean_dir and output_dir != options.output_dir and
+            os.path.exists(output_dir)):
         shutil.rmtree(output_dir)
     builder = Builder(toolchains, output_dir, options.git_dir,
             options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
-            show_unknown=options.show_unknown, step=options.step)
+            show_unknown=options.show_unknown, step=options.step,
+            no_subdirs=options.no_subdirs, full_path=options.full_path,
+            verbose_build=options.verbose_build)
     builder.force_config_on_failure = not options.quick
     if make_func:
         builder.do_make = make_func
index a2a85ac9ce42873b420db6fda98dadc4a707c534..c0ad5d027dce1c3f3e5c56df4e7e242fc007a3da 100644 (file)
@@ -24,6 +24,16 @@ import commit
 import terminal
 import toolchain
 
+settings_data = '''
+# Buildman settings file
+
+[toolchain]
+main: /usr/sbin
+
+[toolchain-alias]
+x86: i386 x86_64
+'''
+
 errors = [
     '''main.c: In function 'main_loop':
 main.c:260:6: warning: unused variable 'joe' [-Wunused-variable]
@@ -83,6 +93,8 @@ boards = [
     ['Active', 'sandbox', 'sandbox', '', 'Tester', 'Sandbox board', 'board4', ''],
 ]
 
+BASE_DIR = 'base'
+
 class Options:
     """Class that holds build options"""
     pass
@@ -111,8 +123,11 @@ class TestBuild(unittest.TestCase):
             self.boards.AddBoard(board.Board(*brd))
         self.boards.SelectBoards([])
 
+        # Add some test settings
+        bsettings.Setup(None)
+        bsettings.AddFile(settings_data)
+
         # 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)
@@ -341,6 +356,64 @@ class TestBuild(unittest.TestCase):
         self.assertEqual(self.boards.SelectBoards(['sandbox sandbox',
                                                    'sandbox']),
                          {'all': 1, 'sandbox': 1})
+    def CheckDirs(self, build, dirname):
+        self.assertEqual('base%s' % dirname, build._GetOutputDir(1))
+        self.assertEqual('base%s/fred' % dirname,
+                         build.GetBuildDir(1, 'fred'))
+        self.assertEqual('base%s/fred/done' % dirname,
+                         build.GetDoneFile(1, 'fred'))
+        self.assertEqual('base%s/fred/u-boot.sizes' % dirname,
+                         build.GetFuncSizesFile(1, 'fred', 'u-boot'))
+        self.assertEqual('base%s/fred/u-boot.objdump' % dirname,
+                         build.GetObjdumpFile(1, 'fred', 'u-boot'))
+        self.assertEqual('base%s/fred/err' % dirname,
+                         build.GetErrFile(1, 'fred'))
+
+    def testOutputDir(self):
+        build = builder.Builder(self.toolchains, BASE_DIR, None, 1, 2,
+                                checkout=False, show_unknown=False)
+        build.commits = self.commits
+        build.commit_count = len(self.commits)
+        subject = self.commits[1].subject.translate(builder.trans_valid_chars)
+        dirname ='/%02d_of_%02d_g%s_%s' % (2, build.commit_count, commits[1][0],
+                                           subject[:20])
+        self.CheckDirs(build, dirname)
+
+    def testOutputDirCurrent(self):
+        build = builder.Builder(self.toolchains, BASE_DIR, None, 1, 2,
+                                checkout=False, show_unknown=False)
+        build.commits = None
+        build.commit_count = 0
+        self.CheckDirs(build, '/current')
+
+    def testOutputDirNoSubdirs(self):
+        build = builder.Builder(self.toolchains, BASE_DIR, None, 1, 2,
+                                checkout=False, show_unknown=False,
+                                no_subdirs=True)
+        build.commits = None
+        build.commit_count = 0
+        self.CheckDirs(build, '')
+
+    def testToolchainAliases(self):
+        self.assertTrue(self.toolchains.Select('arm') != None)
+        with self.assertRaises(ValueError):
+            self.toolchains.Select('no-arch')
+        with self.assertRaises(ValueError):
+            self.toolchains.Select('x86')
+
+        self.toolchains = toolchain.Toolchains()
+        self.toolchains.Add('x86_64-linux-gcc', test=False)
+        self.assertTrue(self.toolchains.Select('x86') != None)
+
+        self.toolchains = toolchain.Toolchains()
+        self.toolchains.Add('i386-linux-gcc', test=False)
+        self.assertTrue(self.toolchains.Select('x86') != None)
+
+    def testToolchainDownload(self):
+        """Test that we can download toolchains"""
+        self.assertEqual('https://www.kernel.org/pub/tools/crosstool/files/bin/x86_64/4.6.3/x86_64-gcc-4.6.3-nolibc_arm-unknown-linux-gnueabi.tar.xz',
+            self.toolchains.LocateArchUrl('arm'))
+
 
 if __name__ == "__main__":
     unittest.main()
index 27dc31889b8021c64d0af0d9bdf2898253e0b993..d4c5d4a11eb96bbea64da6d4e1fb56eea985c4e8 100644 (file)
@@ -5,11 +5,42 @@
 
 import re
 import glob
+from HTMLParser import HTMLParser
 import os
+import sys
+import tempfile
+import urllib2
 
 import bsettings
 import command
 
+# Simple class to collect links from a page
+class MyHTMLParser(HTMLParser):
+    def __init__(self, arch):
+        """Create a new parser
+
+        After the parser runs, self.links will be set to a list of the links
+        to .xz archives found in the page, and self.arch_link will be set to
+        the one for the given architecture (or None if not found).
+
+        Args:
+            arch: Architecture to search for
+        """
+        HTMLParser.__init__(self)
+        self.arch_link = None
+        self.links = []
+        self._match = '_%s-' % arch
+
+    def handle_starttag(self, tag, attrs):
+        if tag == 'a':
+            for tag, value in attrs:
+                if tag == 'href':
+                    if value and value.endswith('.xz'):
+                        self.links.append(value)
+                        if self._match in value:
+                            self.arch_link = value
+
+
 class Toolchain:
     """A single toolchain
 
@@ -20,7 +51,6 @@ class Toolchain:
         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.
 
@@ -30,11 +60,18 @@ class Toolchain:
         """
         self.gcc = fname
         self.path = os.path.dirname(fname)
-        self.cross = os.path.basename(fname)[:-3]
+
+        # Find the CROSS_COMPILE prefix to use for U-Boot. For example,
+        # 'arm-linux-gnueabihf-gcc' turns into 'arm-linux-gnueabihf-'.
+        basename = os.path.basename(fname)
+        pos = basename.rfind('-')
+        self.cross = basename[:pos + 1] if pos != -1 else ''
+
+        # The architecture is the first part of the name
         pos = self.cross.find('-')
         self.arch = self.cross[:pos] if pos != -1 else 'sandbox'
 
-        env = self.MakeEnvironment()
+        env = self.MakeEnvironment(False)
 
         # As a basic sanity check, run the C compiler with --version
         cmd = [fname, '--version']
@@ -74,15 +111,23 @@ class Toolchain:
                 return prio
         return prio
 
-    def MakeEnvironment(self):
+    def MakeEnvironment(self, full_path):
         """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.
+        Thie takes the current environment and adds CROSS_COMPILE so that
+        the tool chain will operate correctly.
+
+        Args:
+            full_path: Return the full path in CROSS_COMPILE and don't set
+                PATH
         """
         env = dict(os.environ)
-        env['CROSS_COMPILE'] = self.cross
-        env['PATH'] += (':' + self.path)
+        if full_path:
+            env['CROSS_COMPILE'] = os.path.join(self.path, self.cross)
+        else:
+            env['CROSS_COMPILE'] = self.cross
+            env['PATH'] = self.path + ':' + env['PATH']
+
         return env
 
 
@@ -101,18 +146,29 @@ class Toolchains:
         self.paths = []
         self._make_flags = dict(bsettings.GetItems('make-flags'))
 
-    def GetSettings(self):
+    def GetPathList(self):
+        """Get a list of available toolchain paths
+
+        Returns:
+            List of strings, each a path to a toolchain mentioned in the
+            [toolchain] section of the settings file.
+        """
         toolchains = bsettings.GetItems('toolchain')
         if not toolchains:
             print ("Warning: No tool chains - please add a [toolchain] section"
                  " to your buildman config file %s. See README for details" %
                  bsettings.config_fname)
 
+        paths = []
         for name, value in toolchains:
             if '*' in value:
-                self.paths += glob.glob(value)
+                paths += glob.glob(value)
             else:
-                self.paths.append(value)
+                paths.append(value)
+        return paths
+
+    def GetSettings(self):
+      self.paths += self.GetPathList()
 
     def Add(self, fname, test=True, verbose=False):
         """Add a toolchain to our list
@@ -132,6 +188,24 @@ class Toolchains:
         if add_it:
             self.toolchains[toolchain.arch] = toolchain
 
+    def ScanPath(self, path, verbose):
+        """Scan a path for a valid toolchain
+
+        Args:
+            path: Path to scan
+            verbose: True to print out progress information
+        Returns:
+            Filename of C compiler if found, else None
+        """
+        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
+                return fname
+        return None
+
+
     def Scan(self, verbose):
         """Scan for available toolchains and select the best for each arch.
 
@@ -145,12 +219,9 @@ class Toolchains:
         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)
+            fname = self.ScanPath(path, verbose)
+            if fname:
+                self.Add(fname, True, verbose)
 
     def List(self):
         """List out the selected toolchains for each architecture"""
@@ -170,9 +241,11 @@ class Toolchains:
         returns:
             toolchain object, or None if none found
         """
-        for name, value in bsettings.GetItems('toolchain-alias'):
-            if arch == name:
-                arch = value
+        for tag, value in bsettings.GetItems('toolchain-alias'):
+            if arch == tag:
+                for alias in value.split():
+                    if alias in self.toolchains:
+                        return self.toolchains[alias]
 
         if not arch in self.toolchains:
             raise ValueError, ("No tool chain found for arch '%s'" % arch)
@@ -247,3 +320,160 @@ class Toolchains:
             else:
                 i += 1
         return args
+
+    def LocateArchUrl(self, fetch_arch):
+        """Find a toolchain available online
+
+        Look in standard places for available toolchains. At present the
+        only standard place is at kernel.org.
+
+        Args:
+            arch: Architecture to look for, or 'list' for all
+        Returns:
+            If fetch_arch is 'list', a tuple:
+                Machine architecture (e.g. x86_64)
+                List of toolchains
+            else
+                URL containing this toolchain, if avaialble, else None
+        """
+        arch = command.OutputOneLine('uname', '-m')
+        base = 'https://www.kernel.org/pub/tools/crosstool/files/bin'
+        versions = ['4.6.3', '4.6.2', '4.5.1', '4.2.4']
+        links = []
+        for version in versions:
+            url = '%s/%s/%s/' % (base, arch, version)
+            print 'Checking: %s' % url
+            response = urllib2.urlopen(url)
+            html = response.read()
+            parser = MyHTMLParser(fetch_arch)
+            parser.feed(html)
+            if fetch_arch == 'list':
+                links += parser.links
+            elif parser.arch_link:
+                return url + parser.arch_link
+        if fetch_arch == 'list':
+            return arch, links
+        return None
+
+    def Download(self, url):
+        """Download a file to a temporary directory
+
+        Args:
+            url: URL to download
+        Returns:
+            Tuple:
+                Temporary directory name
+                Full path to the downloaded archive file in that directory,
+                    or None if there was an error while downloading
+        """
+        print "Downloading: %s" % url
+        leaf = url.split('/')[-1]
+        tmpdir = tempfile.mkdtemp('.buildman')
+        response = urllib2.urlopen(url)
+        fname = os.path.join(tmpdir, leaf)
+        fd = open(fname, 'wb')
+        meta = response.info()
+        size = int(meta.getheaders("Content-Length")[0])
+        done = 0
+        block_size = 1 << 16
+        status = ''
+
+        # Read the file in chunks and show progress as we go
+        while True:
+            buffer = response.read(block_size)
+            if not buffer:
+                print chr(8) * (len(status) + 1), '\r',
+                break
+
+            done += len(buffer)
+            fd.write(buffer)
+            status = r"%10d MiB  [%3d%%]" % (done / 1024 / 1024,
+                                             done * 100 / size)
+            status = status + chr(8) * (len(status) + 1)
+            print status,
+            sys.stdout.flush()
+        fd.close()
+        if done != size:
+            print 'Error, failed to download'
+            os.remove(fname)
+            fname = None
+        return tmpdir, fname
+
+    def Unpack(self, fname, dest):
+        """Unpack a tar file
+
+        Args:
+            fname: Filename to unpack
+            dest: Destination directory
+        Returns:
+            Directory name of the first entry in the archive, without the
+            trailing /
+        """
+        stdout = command.Output('tar', 'xvfJ', fname, '-C', dest)
+        return stdout.splitlines()[0][:-1]
+
+    def TestSettingsHasPath(self, path):
+        """Check if builmand will find this toolchain
+
+        Returns:
+            True if the path is in settings, False if not
+        """
+        paths = self.GetPathList()
+        return path in paths
+
+    def ListArchs(self):
+        """List architectures with available toolchains to download"""
+        host_arch, archives = self.LocateArchUrl('list')
+        re_arch = re.compile('[-a-z0-9.]*_([^-]*)-.*')
+        arch_set = set()
+        for archive in archives:
+            # Remove the host architecture from the start
+            arch = re_arch.match(archive[len(host_arch):])
+            if arch:
+                arch_set.add(arch.group(1))
+        return sorted(arch_set)
+
+    def FetchAndInstall(self, arch):
+        """Fetch and install a new toolchain
+
+        arch:
+            Architecture to fetch, or 'list' to list
+        """
+        # Fist get the URL for this architecture
+        url = self.LocateArchUrl(arch)
+        if not url:
+            print ("Cannot find toolchain for arch '%s' - use 'list' to list" %
+                   arch)
+            return 2
+        home = os.environ['HOME']
+        dest = os.path.join(home, '.buildman-toolchains')
+        if not os.path.exists(dest):
+            os.mkdir(dest)
+
+        # Download the tar file for this toolchain and unpack it
+        tmpdir, tarfile = self.Download(url)
+        if not tarfile:
+            return 1
+        print 'Unpacking to: %s' % dest,
+        sys.stdout.flush()
+        path = self.Unpack(tarfile, dest)
+        os.remove(tarfile)
+        os.rmdir(tmpdir)
+        print
+
+        # Check that the toolchain works
+        print 'Testing'
+        dirpath = os.path.join(dest, path)
+        compiler_fname = self.ScanPath(dirpath, True)
+        if not compiler_fname:
+            print 'Could not locate C compiler - fetch failed.'
+            return 1
+        toolchain = Toolchain(compiler_fname, True, True)
+
+        # Make sure that it will be found by buildman
+        if not self.TestSettingsHasPath(dirpath):
+            print ("Adding 'download' to config file '%s'" %
+                   bsettings.config_fname)
+            tools_dir = os.path.dirname(dirpath)
+            bsettings.SetItem('toolchain', 'download', '%s/*' % tools_dir)
+        return 0
index b68df5d72e7ffa525b84b622840eedce13358a4e..cc5a55ab3ffefd7fce9c0107c9470bbd0d05ed2e 100644 (file)
@@ -61,6 +61,52 @@ def CountCommitsToBranch():
     patch_count = int(stdout)
     return patch_count
 
+def NameRevision(commit_hash):
+    """Gets the revision name for a commit
+
+    Args:
+        commit_hash: Commit hash to look up
+
+    Return:
+        Name of revision, if any, else None
+    """
+    pipe = ['git', 'name-rev', commit_hash]
+    stdout = command.RunPipe([pipe], capture=True, oneline=True).stdout
+
+    # We expect a commit, a space, then a revision name
+    name = stdout.split(' ')[1].strip()
+    return name
+
+def GuessUpstream(git_dir, branch):
+    """Tries to guess the upstream for a branch
+
+    This lists out top commits on a branch and tries to find a suitable
+    upstream. It does this by looking for the first commit where
+    'git name-rev' returns a plain branch name, with no ! or ^ modifiers.
+
+    Args:
+        git_dir: Git directory containing repo
+        branch: Name of branch
+
+    Returns:
+        Tuple:
+            Name of upstream branch (e.g. 'upstream/master') or None if none
+            Warning/error message, or None if none
+    """
+    pipe = [LogCmd(branch, git_dir=git_dir, oneline=True, count=100)]
+    result = command.RunPipe(pipe, capture=True, capture_stderr=True,
+                             raise_on_error=False)
+    if result.return_code:
+        return None, "Branch '%s' not found" % branch
+    for line in result.stdout.splitlines()[1:]:
+        commit_hash = line.split(' ')[0]
+        name = NameRevision(commit_hash)
+        if '~' not in name and '^' not in name:
+            if name.startswith('remotes/'):
+                name = name[8:]
+            return name, "Guessing upstream as '%s'" % name
+    return None, "Cannot find a suitable upstream for branch '%s'" % branch
+
 def GetUpstream(git_dir, branch):
     """Returns the name of the upstream for a branch
 
@@ -69,7 +115,9 @@ def GetUpstream(git_dir, branch):
         branch: Name of branch
 
     Returns:
-        Name of upstream branch (e.g. 'upstream/master') or None if none
+        Tuple:
+            Name of upstream branch (e.g. 'upstream/master') or None if none
+            Warning/error message, or None if none
     """
     try:
         remote = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
@@ -77,13 +125,14 @@ def GetUpstream(git_dir, branch):
         merge = command.OutputOneLine('git', '--git-dir', git_dir, 'config',
                                       'branch.%s.merge' % branch)
     except:
-        return None
+        upstream, msg = GuessUpstream(git_dir, branch)
+        return upstream, msg
 
     if remote == '.':
         return merge
     elif remote and merge:
         leaf = merge.split('/')[-1]
-        return '%s/%s' % (remote, leaf)
+        return '%s/%s' % (remote, leaf), None
     else:
         raise ValueError, ("Cannot determine upstream branch for branch "
                 "'%s' remote='%s', merge='%s'" % (branch, remote, merge))
@@ -99,10 +148,29 @@ def GetRangeInBranch(git_dir, branch, include_upstream=False):
         Expression in the form 'upstream..branch' which can be used to
         access the commits. If the branch does not exist, returns None.
     """
-    upstream = GetUpstream(git_dir, branch)
+    upstream, msg = GetUpstream(git_dir, branch)
     if not upstream:
-        return None
-    return '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
+        return None, msg
+    rstr = '%s%s..%s' % (upstream, '~' if include_upstream else '', branch)
+    return rstr, msg
+
+def CountCommitsInRange(git_dir, range_expr):
+    """Returns the number of commits in the given range.
+
+    Args:
+        git_dir: Directory containing git repo
+        range_expr: Range to check
+    Return:
+        Number of patches that exist in the supplied rangem or None if none
+        were found
+    """
+    pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True)]
+    result = command.RunPipe(pipe, capture=True, capture_stderr=True,
+                             raise_on_error=False)
+    if result.return_code:
+        return None, "Range '%s' not found or is invalid" % range_expr
+    patch_count = len(result.stdout.splitlines())
+    return patch_count, None
 
 def CountCommitsInBranch(git_dir, branch, include_upstream=False):
     """Returns the number of commits in the given branch.
@@ -114,14 +182,10 @@ def CountCommitsInBranch(git_dir, branch, include_upstream=False):
         Number of patches that exist on top of the branch, or None if the
         branch does not exist.
     """
-    range_expr = GetRangeInBranch(git_dir, branch, include_upstream)
+    range_expr, msg = GetRangeInBranch(git_dir, branch, include_upstream)
     if not range_expr:
-        return None
-    pipe = [LogCmd(range_expr, git_dir=git_dir, oneline=True),
-            ['wc', '-l']]
-    result = command.RunPipe(pipe, capture=True, oneline=True)
-    patch_count = int(result.stdout)
-    return patch_count
+        return None, msg
+    return CountCommitsInRange(git_dir, range_expr)
 
 def CountCommits(commit_range):
     """Returns the number of commits in the given range.