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