Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-01-18 09:15:25

0001 # python3 build_images.py <flags> <name>
0002 # 
0003 # to build and push the "latest" version:
0004 #    python3 build_images.py --no-cache --push eic
0005 # to build and push the tagged version
0006 #    python3 build_images.py --no-cache --push --tag=<name> eic
0007 #
0008 # Flags:
0009 #  --no-cache - clean build
0010 #  --push     - push after build
0011 #  --tag      - tag name of an image (this is like "latest", not full docker name)
0012 #  --latest   - add 'latest' tag to this image too
0013 # Names:
0014 #    eic - collection of eic images
0015 #    devops - devops images,
0016 #    or image name without namespace: escalate
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 # create formatter
0037 # formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
0038 # create console handler with a higher log level
0039 ch = logging.StreamHandler()
0040 
0041 # Set pathes and global variables
0042 this_path = os.path.dirname(os.path.abspath(inspect.stack()[0][1]))
0043 
0044 # Number of CPU-s
0045 cpu_count = multiprocessing.cpu_count()
0046 if cpu_count > 1:
0047     cpu_count -= 1  # Leave 1 CPU to a user. Ha. Ha. Ha.
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  # Additional flags needed to build
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     # Pretty header for the command
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     # Record the start time
0097     start_time = datetime.now()
0098     lines = []
0099 
0100     # stderr is redirected to STDOUT because otherwise it needs special handling
0101     # we don't need it, and we don't care as C++ warnings generate too much stderr
0102     # which makes it pretty much like stdout
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     # Get return value and finishing time
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]):  # like "ejana-centos7-prereq"
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         # no-cache flag given?
0164         no_cache_str = "--no-cache" if self.no_cache else ""
0165 
0166         # RUN DOCKER BUILD COMMAND
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         # Log the results:
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         # Add to built images list built 
0181         self.built_images_by_name[image.alias] = image
0182 
0183         # Tag this build as latest
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             # Log the results:
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         # Push image after built
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         # Log the results:
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         # Push also the latest branch
0220         if self.tag_latest:
0221             retval, start_time, end_time, output = _run(f"docker push {image.tag_latest_name}")
0222 
0223             # Log the results:
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     # Argument parsing
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         # create file handler which logs even debug messages
0251         fh.setLevel(logging.DEBUG)
0252         ch.setLevel(logging.INFO)
0253         logger.addHandler(fh)
0254     else:
0255         ch.setLevel(logging.DEBUG)
0256 
0257     # add the handlers to the logger
0258     logger.addHandler(ch)
0259 
0260     # What images to build
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     # Number of jobs to build
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     # import json
0299     # with open('result.json', 'w') as outfile:
0300     #    json.dump(logs, outfile, indent=4, ensure_ascii=False)
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)