Bumps OVS version to 2.8 for OVN
[apex.git] / ci / build.py
1 ##############################################################################
2 # Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10 import argparse
11 import logging
12 import os
13 import subprocess
14 import sys
15 import uuid
16 import yaml
17
18 CACHE_JOURNAL = 'cache_journal.yaml'
19 TMP_CACHE = '.cache'
20 BUILD_ROOT = 'build'
21 BUILD_LOG_FILE = './apex_build.log'
22
23 class ApexBuildException(Exception):
24     pass
25
26
27 def create_build_parser():
28     build_parser = argparse.ArgumentParser()
29     build_parser.add_argument('--debug', action='store_true', default=False,
30                               help="Turn on debug messages")
31     build_parser.add_argument('-l', '--log-file',
32                               default=BUILD_LOG_FILE,
33                               dest='log_file', help="Log file to log to")
34     build_parser.add_argument('-c', '--cache-dir',
35                               dest='cache_dir',
36                               default=None,
37                               help='Directory to store cache')
38     build_parser.add_argument('--iso', action='store_true',
39                               default=False,
40                               help='Build ISO image')
41     build_parser.add_argument('--rpms', action='store_true',
42                               default=False,
43                               help='Build RPMs')
44     build_parser.add_argument('-r', '--release',
45                               dest='build_version',
46                               help='Version to apply to build '
47                                    'artifact label')
48
49     return build_parser
50
51
52 def get_journal(cache_dir):
53     """
54     Search for the journal file and returns its contents
55     :param cache_dir: cache storage directory where journal file is
56     :return: content of journal file
57     """
58     journal_file = "{}/{}".format(cache_dir, CACHE_JOURNAL)
59     if os.path.isfile(journal_file) is False:
60         logging.info("Journal file not found {}, skipping cache search".format(
61             journal_file))
62     else:
63         with open(journal_file, 'r') as fh:
64             cache_journal = yaml.safe_load(fh)
65             assert isinstance(cache_journal, list)
66             return cache_journal
67
68
69 def get_cache_file(cache_dir):
70     """
71     Searches for a valid cache entry in the cache journal
72     :param cache_dir: directory where cache and journal are located
73     :return: name of valid cache file
74     """
75     cache_journal = get_journal(cache_dir)
76     if cache_journal is not None:
77         valid_cache = cache_journal[-1]
78         if os.path.isfile(valid_cache):
79             return valid_cache
80
81
82 def unpack_cache(cache_dest, cache_dir=None):
83     if cache_dir is None:
84         logging.info("Cache directory not provided, skipping cache unpack")
85         return
86     elif os.path.isdir(cache_dir) is False:
87         logging.info("Cache Directory does not exist, skipping cache unpack")
88         return
89     else:
90         logging.info("Cache Directory Found: {}".format(cache_dir))
91         cache_file = get_cache_file(cache_dir)
92         if cache_file is None:
93             logging.info("No cache file detected, skipping cache unpack")
94             return
95         logging.info("Unpacking Cache {}".format(cache_file))
96         if not os.path.exists(cache_dest):
97             os.makedirs(cache_dest)
98         try:
99             subprocess.check_call(["tar", "xvf", cache_file, "-C", cache_dest])
100         except subprocess.CalledProcessError:
101             logging.warning("Cache unpack failed")
102             return
103         logging.info("Cache unpacked, contents are: {}",
104                      os.listdir(cache_dest))
105
106
107 def build(build_root, version, iso=False, rpms=False):
108     if iso:
109         make_targets = ['iso']
110     elif rpms:
111         make_targets = ['rpms']
112     else:
113         make_targets = ['images', 'rpms-check']
114     if version is not None:
115         make_args = ['RELEASE={}'.format(version)]
116     else:
117         make_args = []
118     logging.info('Building targets: {}'.format(make_targets))
119     try:
120         output = subprocess.check_output(["make"] + make_args + ["-C",
121                                          build_root] + make_targets)
122         logging.info(output)
123     except subprocess.CalledProcessError as e:
124         logging.error("Failed to build Apex artifacts")
125         logging.error(e.output)
126         raise e
127
128
129 def build_cache(cache_source, cache_dir):
130     """
131     Tar up new cache with unique name and store it in cache storage
132     directory.  Also update journal file with new cache entry.
133     :param cache_source: source files to tar up when building cache file
134     :param cache_dir: cache storage location
135     :return: None
136     """
137     if cache_dir is None:
138         logging.info("No cache dir specified, will not build cache")
139         return
140     cache_name = 'apex-cache-{}.tgz'.format(str(uuid.uuid4()))
141     cache_full_path = os.path.join(cache_dir, cache_name)
142     os.makedirs(cache_dir, exist_ok=True)
143     try:
144         subprocess.check_call(['tar', '--atime-preserve', '--dereference',
145                                '-caf', cache_full_path, '-C', cache_source,
146                                '.'])
147     except BaseException as e:
148         logging.error("Unable to build new cache tarball")
149         if os.path.isfile(cache_full_path):
150             os.remove(cache_full_path)
151         raise e
152     if os.path.isfile(cache_full_path):
153         logging.info("Cache Build Complete")
154         # update journal
155         cache_entries = get_journal(cache_dir)
156         if cache_entries is None:
157             cache_entries = [cache_name]
158         else:
159             cache_entries.append(cache_name)
160         journal_file = os.path.join(cache_dir, CACHE_JOURNAL)
161         with open(journal_file, 'w') as fh:
162             yaml.safe_dump(cache_entries, fh, default_flow_style=False)
163         logging.info("Journal updated with new entry: {}".format(cache_name))
164     else:
165         logging.warning("Cache file did not build correctly")
166
167
168 def prune_cache(cache_dir):
169     """
170     Remove older cache entries if there are more than 2
171     :param cache_dir: Cache storage directory
172     :return: None
173     """
174     if cache_dir is None:
175         return
176     cache_modified_flag = False
177     cache_entries = get_journal(cache_dir)
178     while len(cache_entries) > 2:
179         logging.debug("Will remove older cache entries")
180         cache_to_rm = cache_entries[0]
181         cache_full_path = os.path.join(cache_dir, cache_to_rm)
182         if os.path.isfile(cache_full_path):
183             try:
184                 os.remove(cache_full_path)
185                 cache_entries.pop(0)
186                 cache_modified_flag = True
187             except os.EX_OSERR:
188                 logging.warning("Failed to remove cache file: {}".format(
189                     cache_full_path))
190                 break
191
192     else:
193         logging.debug("No more cache cleanup necessary")
194
195     if cache_modified_flag:
196         logging.debug("Updating cache journal")
197         journal_file = os.path.join(cache_dir, CACHE_JOURNAL)
198         with open(journal_file, 'w') as fh:
199             yaml.safe_dump(cache_entries, fh, default_flow_style=False)
200
201 if __name__ == '__main__':
202     parser = create_build_parser()
203     args = parser.parse_args(sys.argv[1:])
204     if args.debug:
205         log_level = logging.DEBUG
206     else:
207         log_level = logging.INFO
208     os.makedirs(os.path.dirname(args.log_file), exist_ok=True)
209     formatter = '%(asctime)s %(levelname)s: %(message)s'
210     logging.basicConfig(filename=args.log_file,
211                         format=formatter,
212                         datefmt='%m/%d/%Y %I:%M:%S %p',
213                         level=log_level)
214     console = logging.StreamHandler()
215     console.setLevel(log_level)
216     console.setFormatter(logging.Formatter(formatter))
217     logging.getLogger('').addHandler(console)
218     apex_root = os.path.split(os.getcwd())[0]
219     for root, dirs, files in os.walk(apex_root):
220         if BUILD_ROOT in dirs:
221             apex_root = root
222     apex_build_root = os.path.join(apex_root, BUILD_ROOT)
223     if os.path.isdir(apex_build_root):
224         cache_tmp_dir = os.path.join(apex_root, TMP_CACHE)
225     else:
226         logging.error("You must execute this script inside of the Apex "
227                       "local code repository")
228         raise ApexBuildException("Invalid path for apex root: {}.  Must be "
229                                  "invoked from within Apex code directory.".
230                                  format(apex_root))
231     unpack_cache(cache_tmp_dir, args.cache_dir)
232     build(apex_build_root, args.build_version, args.iso, args.rpms)
233     build_cache(cache_tmp_dir, args.cache_dir)
234     prune_cache(args.cache_dir)