buildman: Add a --boards option to specify particular boards to build

At present 'buildman sandbox' will build all 5 boards for the sandbox
architecture rather than the single board 'sandbox'. The only current way
to exclude sandbox_spl, sandbox_noblk, etc. is to use -x which is a bit
clumbsy.

Add a --boards option to allow individual build targets to be specified.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/buildman/README b/tools/buildman/README
index 7660190..5a709c6 100644
--- a/tools/buildman/README
+++ b/tools/buildman/README
@@ -114,6 +114,10 @@
 plan to use your machine for anything else, you can use -T to increase the
 number of threads beyond the default.
 
+
+Selecting which boards to build
+===============================
+
 Buildman lets you build all boards, or a subset. Specify the subset by passing
 command-line arguments that list the desired board name, architecture name,
 SOC name, or anything else in the boards.cfg file. Multiple arguments are
@@ -138,11 +142,17 @@
 means to build all arm boards except nvidia, freescale and anything ending
 with 'ball'.
 
+For building specific boards you can use the --boards option, which takes a
+comma-separated list of board target names and be used multiple times on
+the command line:
+
+   buidman --boards sandbox,snow --boards
+
 It is convenient to use the -n option to see what will be built based on
 the subset given. Use -v as well to get an actual list of boards.
 
 Buildman does not store intermediate object files. It optionally copies
-the binary output into a directory when a build is successful. Size
+the binary output into a directory when a build is successful (-k). Size
 information is always recorded. It needs a fair bit of disk space to work,
 typically 250MB per thread.
 
diff --git a/tools/buildman/board.py b/tools/buildman/board.py
index 272bac0..2a1d021 100644
--- a/tools/buildman/board.py
+++ b/tools/buildman/board.py
@@ -237,20 +237,30 @@
             terms.append(term)
         return terms
 
-    def SelectBoards(self, args, exclude=[]):
+    def SelectBoards(self, args, exclude=[], boards=None):
         """Mark boards selected based on args
 
+        Normally either boards (an explicit list of boards) or args (a list of
+        terms to match against) is used. It is possible to specify both, in
+        which case they are additive.
+
+        If boards and args are both empty, all boards are selected.
+
         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.
             exclude: List of boards to exclude, regardless of 'args'
+            boards: List of boards to build
 
         Returns:
-            Dictionary which holds the list of boards which were selected
-            due to each argument, arranged by argument.
+            Tuple
+                Dictionary which holds the list of boards which were selected
+                    due to each argument, arranged by argument.
+                List of errors found
         """
         result = {}
+        warnings = []
         terms = self._BuildTerms(args)
 
         result['all'] = []
@@ -261,6 +271,7 @@
         for expr in exclude:
             exclude_list.append(Expr(expr))
 
+        found = []
         for board in self._boards:
             matching_term = None
             build_it = False
@@ -271,6 +282,10 @@
                         matching_term = str(term)
                         build_it = True
                         break
+            elif boards:
+                if board.target in boards:
+                    build_it = True
+                    found.append(board.target)
             else:
                 build_it = True
 
@@ -286,4 +301,9 @@
                     result[matching_term].append(board.target)
                 result['all'].append(board.target)
 
-        return result
+        if boards:
+            remaining = set(boards) - set(found)
+            if remaining:
+                warnings.append('Boards not found: %s\n' % ', '.join(remaining))
+
+        return result, warnings
diff --git a/tools/buildman/cmdline.py b/tools/buildman/cmdline.py
index e493b1a..49a8a13 100644
--- a/tools/buildman/cmdline.py
+++ b/tools/buildman/cmdline.py
@@ -18,6 +18,8 @@
     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('--boards', type='string', action='append',
+          help='List of board names to build separated by comma')
     parser.add_option('-c', '--count', dest='count', type='int',
           default=-1, help='Run build on the top n commits')
     parser.add_option('-C', '--force-reconfig', dest='force_reconfig',
@@ -102,7 +104,7 @@
           type='string', action='append',
           help='Specify a list of boards to exclude, separated by comma')
 
-    parser.usage += """
+    parser.usage += """ [list of target/arch/cpu/board/vendor/soc to build]
 
     Build U-Boot for all commits in a branch. Use -n to do a dry run"""
 
diff --git a/tools/buildman/control.py b/tools/buildman/control.py
index bc08197..96f8ccf 100644
--- a/tools/buildman/control.py
+++ b/tools/buildman/control.py
@@ -41,7 +41,8 @@
             GetPlural(options.threads), options.jobs, GetPlural(options.jobs))
     return str
 
-def ShowActions(series, why_selected, boards_selected, builder, options):
+def ShowActions(series, why_selected, boards_selected, builder, options,
+                board_warnings):
     """Display a list of actions that we would take, if not a dry run.
 
     Args:
@@ -55,6 +56,7 @@
                 value is Board object
         builder: The builder that will be used to build the commits
         options: Command line options object
+        board_warnings: List of warnings obtained from board selected
     """
     col = terminal.Color()
     print 'Dry run, so not doing much. But I would do this:'
@@ -79,6 +81,9 @@
                 print '   %s' % ' '.join(why_selected[arg])
     print ('Total boards to build for each commit: %d\n' %
             len(why_selected['all']))
+    if board_warnings:
+        for warning in board_warnings:
+            print col.Color(col.YELLOW, warning)
 
 def CheckOutputDir(output_dir):
     """Make sure that the output directory is not within the current directory
@@ -210,7 +215,15 @@
         for arg in options.exclude:
             exclude += arg.split(',')
 
-    why_selected = boards.SelectBoards(args, exclude)
+
+    if options.boards:
+        requested_boards = []
+        for b in options.boards:
+            requested_boards += b.split(',')
+    else:
+        requested_boards = None
+    why_selected, board_warnings = boards.SelectBoards(args, exclude,
+                                                       requested_boards)
     selected = boards.GetSelected()
     if not len(selected):
         sys.exit(col.Color(col.RED, 'No matching boards found'))
@@ -292,7 +305,8 @@
 
     # For a dry run, just show our actions as a sanity check
     if options.dry_run:
-        ShowActions(series, why_selected, selected, builder, options)
+        ShowActions(series, why_selected, selected, builder, options,
+                    board_warnings)
     else:
         builder.force_build = options.force_build
         builder.force_build_failures = options.force_build_failures
diff --git a/tools/buildman/test.py b/tools/buildman/test.py
index e0c9d6d..61a4626 100644
--- a/tools/buildman/test.py
+++ b/tools/buildman/test.py
@@ -313,60 +313,63 @@
     def testBoardSingle(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards(['sandbox']),
-                         {'all': ['board4'], 'sandbox': ['board4']})
+                         ({'all': ['board4'], 'sandbox': ['board4']}, []))
 
     def testBoardArch(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards(['arm']),
-                         {'all': ['board0', 'board1'],
-                          'arm': ['board0', 'board1']})
+                         ({'all': ['board0', 'board1'],
+                          'arm': ['board0', 'board1']}, []))
 
     def testBoardArchSingle(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards(['arm sandbox']),
-                         {'sandbox': ['board4'],
+                         ({'sandbox': ['board4'],
                           'all': ['board0', 'board1', 'board4'],
-                          'arm': ['board0', 'board1']})
+                          'arm': ['board0', 'board1']}, []))
 
 
     def testBoardArchSingleMultiWord(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards(['arm', 'sandbox']),
-                         {'sandbox': ['board4'], 'all': ['board0', 'board1', 'board4'], 'arm': ['board0', 'board1']})
+                         ({'sandbox': ['board4'],
+                          'all': ['board0', 'board1', 'board4'],
+                          'arm': ['board0', 'board1']}, []))
 
     def testBoardSingleAnd(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards(['Tester & arm']),
-                         {'Tester&arm': ['board0', 'board1'], 'all': ['board0', 'board1']})
+                         ({'Tester&arm': ['board0', 'board1'],
+                           'all': ['board0', 'board1']}, []))
 
     def testBoardTwoAnd(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards(['Tester', '&', 'arm',
                                                    'Tester' '&', 'powerpc',
                                                    'sandbox']),
-                         {'sandbox': ['board4'],
+                         ({'sandbox': ['board4'],
                           'all': ['board0', 'board1', 'board2', 'board3',
                                   'board4'],
                           'Tester&powerpc': ['board2', 'board3'],
-                          'Tester&arm': ['board0', 'board1']})
+                          'Tester&arm': ['board0', 'board1']}, []))
 
     def testBoardAll(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards([]),
-                         {'all': ['board0', 'board1', 'board2', 'board3',
-                                  'board4']})
+                         ({'all': ['board0', 'board1', 'board2', 'board3',
+                                  'board4']}, []))
 
     def testBoardRegularExpression(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards(['T.*r&^Po']),
-                         {'all': ['board2', 'board3'],
-                          'T.*r&^Po': ['board2', 'board3']})
+                         ({'all': ['board2', 'board3'],
+                          'T.*r&^Po': ['board2', 'board3']}, []))
 
     def testBoardDuplicate(self):
         """Test single board selection"""
         self.assertEqual(self.boards.SelectBoards(['sandbox sandbox',
                                                    'sandbox']),
-                         {'all': ['board4'], 'sandbox': ['board4']})
+                         ({'all': ['board4'], 'sandbox': ['board4']}, []))
     def CheckDirs(self, build, dirname):
         self.assertEqual('base%s' % dirname, build._GetOutputDir(1))
         self.assertEqual('base%s/fred' % dirname,