File indexing completed on 2025-01-18 09:15:25
0001
0002
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
0019 import inspect
0020 import json
0021 import os
0022 import pathlib
0023 import shlex
0024 import subprocess
0025 import argparse
0026 import multiprocessing
0027 from datetime import datetime
0028 from typing import Tuple, Union, List, Dict
0029 import logging
0030 import time
0031
0032 logger = logging.getLogger()
0033 logger.setLevel(logging.DEBUG)
0034
0035 fh = logging.FileHandler('build.log')
0036
0037
0038
0039 ch = logging.StreamHandler()
0040
0041
0042 this_path = os.path.dirname(os.path.abspath(inspect.stack()[0][1]))
0043
0044
0045 cpu_count = multiprocessing.cpu_count()
0046 if cpu_count > 1:
0047 cpu_count -= 1
0048
0049 logger.debug("CPU COUNT ", cpu_count)
0050
0051
0052 class ImageInfo:
0053 def __init__(self, category: str = '', org: str = '', alias: str = '', name: str = '', image_path: str = '',
0054 tag: str = '', depends_on: str = '', flags: str = ''):
0055 """
0056 # Alias can be used to have the ImageInfo with the same name but different flags
0057 """
0058 self.category = category
0059 self.organization = org
0060 self.alias = alias
0061 self.name = name
0062 if not alias:
0063 self.alias = self.name
0064 self.tag = tag
0065 self.depends_on = depends_on
0066 self.path = image_path
0067 self.flags = flags
0068
0069 @property
0070 def full_name(self):
0071 return f"{self.organization}/{self.name}:{self.tag}"
0072
0073 @property
0074 def tag_latest_name(self):
0075 return f"{self.organization}/{self.name}:latest"
0076
0077 def __repr__(self):
0078 return f"Image '{self.full_name}'"
0079
0080
0081 def _run(command: Union[str, list]) -> Tuple[int, datetime, datetime, List]:
0082 """Wrapper around subprocess.Popen that returns:
0083
0084 :return retval, start_time, end_time, lines
0085
0086 """
0087 if isinstance(command, str):
0088 command = shlex.split(command)
0089
0090
0091 pretty_header = "RUN: " + " ".join(command)
0092 logger.info('=' * len(pretty_header))
0093 logger.info(pretty_header)
0094 logger.info('=' * len(pretty_header))
0095
0096
0097 start_time = datetime.now()
0098 lines = []
0099
0100
0101
0102
0103 process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
0104 while True:
0105 time.sleep(0)
0106
0107 output = process.stdout.readline().decode('latin-1')
0108
0109 if process.poll() is not None and output == '':
0110 break
0111 if output:
0112 fh_term = fh.terminator
0113 ch_term = ch.terminator
0114 fh.terminator = ""
0115 ch.terminator = ""
0116 logger.debug(output)
0117 fh.terminator = fh_term
0118 ch.terminator = ch_term
0119 lines.append(output)
0120
0121
0122 retval = process.poll()
0123 end_time = datetime.now()
0124
0125 logger.info("------------------------------------------")
0126 logger.info(f"RUN DONE. RETVAL: {retval} \n\n")
0127
0128 return retval, start_time, end_time, lines
0129
0130
0131 class DockerAutomation(object):
0132
0133 def __init__(self, images: Dict[str, ImageInfo]):
0134 self.operation_logs: List[dict] = []
0135 self.images_by_name = images
0136 self.check_deps = True
0137 self.no_cache = False
0138 self.push_after_build = True
0139 self.tag_latest = False
0140 self.built_images_by_name = {}
0141
0142 def _append_log(self, op, name, ret_code, start_time, end_time, output):
0143 """Saves data to specially formatted record"""
0144 duration = end_time - start_time
0145 self.operation_logs.append({'op': op,
0146 'name': name,
0147 'ret_code': ret_code,
0148 'start_time': start_time,
0149 'start_time_str': start_time.strftime("%Y-%m-%d %H:%M:%S"),
0150 'end_time': end_time,
0151 'end_time_str': end_time.strftime("%Y-%m-%d %H:%M:%S"),
0152 'duration': duration,
0153 'duration_str': str(duration)[:9],
0154 'output': output})
0155
0156 def _build_image(self, image: ImageInfo):
0157 """
0158 docker build --tag=ejana-centos7-prereq .
0159 docker tag ejana-centos7-prereq eicdev/ejana-centos7-prereq:latest
0160 docker push eicdev/ejana-centos7-prereq:latest
0161 """
0162
0163
0164 no_cache_str = "--no-cache" if self.no_cache else ""
0165
0166
0167 logger.debug(f"image.path = {image.path}")
0168
0169 os.chdir(image.path)
0170 retval, start_time, end_time, output = _run(
0171 f"docker build {no_cache_str} {image.flags} --tag={image.full_name} .")
0172
0173
0174 self._append_log('build', image.full_name, retval, start_time, end_time, output)
0175
0176 if retval:
0177 logger.error(f"(! ! !) ERROR (! ! !) build op return code is: {retval}")
0178 return
0179
0180
0181 self.built_images_by_name[image.alias] = image
0182
0183
0184 if self.tag_latest:
0185 retval, start_time, end_time, output = _run(f"docker tag {image.full_name} {image.tag_latest_name}")
0186
0187
0188 self._append_log('tag-latest', image.tag_latest_name, retval, start_time, end_time, output)
0189
0190 if retval:
0191 logger.error(f"(! ! !) ERROR (! ! !) tag latest return code is: {retval}")
0192
0193
0194 if self.push_after_build:
0195 self.push(image)
0196
0197 def build(self, image_name: str):
0198 self._build_image(self.images_by_name[image_name])
0199
0200 def build_all(self):
0201 images = self.images_by_name.values()
0202 for image in images:
0203 self._build_image(image)
0204
0205 def push(self, name_or_image):
0206 if isinstance(name_or_image, ImageInfo):
0207 image = name_or_image
0208 else:
0209 image = self.images_by_name[name_or_image]
0210 os.chdir(image.path)
0211 retval, start_time, end_time, output = _run(f"docker push {image.full_name}")
0212
0213
0214 self._append_log('push', image.full_name, retval, start_time, end_time, output)
0215
0216 if retval:
0217 logger.error(f"(! ! !) ERROR (! ! !) PUSH operation return code is: {retval}")
0218
0219
0220 if self.tag_latest:
0221 retval, start_time, end_time, output = _run(f"docker push {image.tag_latest_name}")
0222
0223
0224 self._append_log('push-latest', image.tag_latest_name, retval, start_time, end_time, output)
0225
0226 if retval:
0227 logger.error(f"(! ! !) ERROR (! ! !) tag latest return code is: {retval}")
0228
0229 def push_all(self):
0230 for name in self.images_by_name.keys():
0231 self.push(name)
0232
0233
0234 def main():
0235
0236 cwd = os.getcwd()
0237 parser = argparse.ArgumentParser()
0238 parser.add_argument("--no-cache", help="Use docker --no-cache flag during build", action="store_true")
0239 parser.add_argument("--tag", help="Set version tag name. latest is set by default", default='dev')
0240 parser.add_argument("--push", action="store_true", help="If true - push images if built successfully")
0241 parser.add_argument("--latest", action="store_true", help="If true - also tag this image as 'latest' tag")
0242 parser.add_argument("--log-to-file", action="store_true", help="Log to file instead of stdout")
0243 parser.add_argument("--check-deps", type=bool, help="Check that dependency is built", default=True)
0244 parser.add_argument("-j", "--jobs", type=int, default=cpu_count, help="Number of parallel jobs")
0245
0246 parser.add_argument("command", type=str, nargs="*", help="directories with Dockerfile")
0247 args = parser.parse_args()
0248
0249 if args.log_to_file:
0250
0251 fh.setLevel(logging.DEBUG)
0252 ch.setLevel(logging.INFO)
0253 logger.addHandler(fh)
0254 else:
0255 ch.setLevel(logging.DEBUG)
0256
0257
0258 logger.addHandler(ch)
0259
0260
0261 if not args.command:
0262 print("No image is provided, using default")
0263 args.command = ['eicrecon-ubuntu22-prereq', 'eicrecon-ubuntu22', 'jana4ml4fpga-ubuntu22', 'ml4fpga-pre']
0264
0265 print(f"Images: {args.command} (arg type of {type(args.command)})")
0266
0267
0268 print(f"Number of jobs: {args.jobs}")
0269
0270 images = {}
0271 for image_name in args.command:
0272 images[image_name] = ImageInfo(
0273 name=image_name,
0274 image_path=os.path.join(this_path, image_name),
0275 org='electronioncollider' if "ml4fpga" in image_name else 'eicdev',
0276 tag=args.tag,
0277 flags=f'--build-arg BUILD_THREADS={args.jobs}'
0278 )
0279 automation = DockerAutomation(images)
0280 automation.no_cache = args.no_cache
0281 automation.push_after_build = args.push
0282 automation.tag_latest = args.latest
0283
0284 automation.build_all()
0285 logs = automation.operation_logs
0286 os.chdir(cwd)
0287 error_code = 0
0288
0289 logger.info('SUMMARY:')
0290 logger.info("{:<12} {:<38} {:<9} {:<11} {:<21} {:<21}"
0291 .format('ACTION', 'IMAGE NAME', 'RETCODE', 'DURATION', 'START TIME', 'END TIME'))
0292 for log in logs:
0293 logger.info(
0294 "{op:<12} {name:<38} {ret_code:<9} {duration_str:<11} {start_time_str:<21} {end_time_str:<21}".format(
0295 **log))
0296 if log['ret_code'] != 0:
0297 error_code = log['ret_code']
0298
0299
0300
0301 return error_code, logs
0302
0303
0304 if __name__ == '__main__':
0305 ret_code, _ = main()
0306
0307 if ret_code != 0:
0308 exit(ret_code)