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