tasks/tools/build_tool.py (264 lines of code) (raw):

# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance # with the License. A copy of the License is located at # # http://www.apache.org/licenses/LICENSE-2.0 # # or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions # and limitations under the License. import tasks.idea as idea from invoke import Context from typing import Optional import shutil import os from abc import abstractmethod, ABC class BaseMetadataUpdater(ABC): @abstractmethod def update(self): ... class PythonAppMetaFileUpdater(BaseMetadataUpdater): """ Update SOCA Release Version in Python App: <packagename>_meta/__init__ file This enables applications to find SOCA Release version and current version without relying on Environment Variables """ def __init__(self, meta_file: str): if not os.path.isfile(meta_file): raise idea.exceptions.invalid_params(f'meta_file: {meta_file} does not exist of not found') self.meta_file = meta_file def update(self): version_var_name = '__version__' release_version = idea.props.idea_release_version with open(self.meta_file, 'r') as f: lines = f.readlines() result = [] found = False for line in lines: if len(line.strip()) == 0: result.append(line) continue if not line.strip().startswith(version_var_name): result.append(line) continue result.append(f"{version_var_name} = '{release_version}'") found = True break if not found: result.append(f"{version_var_name} = '{release_version}'") result.append(os.linesep) with open(self.meta_file, 'w') as f: f.write(''.join(result)) class InstallScriptsFileUpdater(BaseMetadataUpdater): def update(self): pass class WebAppEnvFileUpdater(BaseMetadataUpdater): def __init__(self, webapp_env_file: str, app_name: str, app_version: str, release_version: str): if not os.path.isfile(webapp_env_file): raise idea.exceptions.invalid_params(f'webapp .env file: {webapp_env_file} does not exist of not found') self.webapp_env_file = webapp_env_file self.app_name = app_name self.app_version = app_version self.release_version = release_version def update(self): var_release_version = 'REACT_APP_IDEA_RELEASE_VERSION' release_version = f'{var_release_version}="{self.release_version}"{os.linesep}' release_version_updated = False with open(self.webapp_env_file, 'r') as f: lines = f.readlines() updates = [] for line in lines: updated = line if updated.startswith(var_release_version): updated = release_version release_version_updated = True updates.append(updated) # takes care of scenarios when the env var does not exist in the file or a new env is added in future. if not release_version_updated: updates.append(release_version) webapp_env_file_contents = ''.join(updates) with open(self.webapp_env_file, 'w') as f: f.write(webapp_env_file_contents) class NpmPackageJsonFileUpdater(BaseMetadataUpdater): """ Update the package.json name and version using setup.py name and version Helps keep python app and web app name and versions in-sync should be called everytime during build """ def __init__(self, package_json_file: str, app_name: str, app_version: str, release_version: str): if not os.path.isfile(package_json_file): raise idea.exceptions.invalid_params(f'package.json file: {package_json_file} does not exist of not found') self.package_json_file = package_json_file self.app_name = app_name self.app_version = app_version self.release_version = release_version def update(self): with open(self.package_json_file, 'r') as f: lines = f.readlines() updates = [] for line in lines: updated = line token = updated.strip() if token.startswith('"name"'): updated = f' "name": "web-portal", {os.linesep}' elif token.startswith('"version"'): updated = f' "version": "{self.app_version}", {os.linesep}' updates.append(updated) package_json_contents = ''.join(updates) with open(self.package_json_file, 'w') as f: f.write(package_json_contents) class BuildTool: """ IDEA Project Build Tool Handles building of individual projects under <PROJECT_ROOT>/source/idea/* Works based on standard idea directory structure: <PROJECT_ROOT>/ + source/ + idea/ + <project-name>/ + src/ + <projectname>/ + <projectname>_meta/ + __init__.py + setup.py + resources/ + config/ + webapp?/ + scripts/ Build outputs will be available under: <PROJECT_ROOT>/ + build/ + <project-name>/ """ def __init__(self, c: Context, app_name: str): self.c = c if app_name is None: raise idea.exceptions.invalid_params('app_name is required') app_dir = os.path.join(idea.props.project_source_dir, app_name) if not os.path.isdir(app_dir): raise idea.exceptions.invalid_params(f'project_dir: {app_dir} not found or does not exist') self.app_dir = app_dir self.release_version = idea.props.idea_release_version self._given_app_name = app_name self._app_name: Optional[str] = None @property def app_name(self) -> str: if self._app_name is not None: return self._app_name if self.has_src(): self._app_name = idea.utils.get_package_meta(self.c, self.src_dir, 'name') return self._app_name else: return self._given_app_name @property def app_version(self) -> str: return idea.props.idea_release_version @property def output_dir(self) -> str: return os.path.join(idea.props.project_build_dir, self.output_archive_basename) @property def output_archive_basename(self) -> str: return self.app_name @property def output_archive_name(self) -> str: return f'{self.output_archive_basename}.tar.gz' @property def output_archive_file(self) -> str: return os.path.join(idea.props.project_build_dir, self.output_archive_name) @property def src_dir(self) -> str: return os.path.join(self.app_dir, 'src') def has_src(self) -> bool: return os.path.isdir(self.src_dir) @property def webapp_dir(self) -> str: return os.path.join(self.app_dir, 'webapp') @property def webapp_build_dir(self) -> str: return os.path.join(self.webapp_dir, 'build') def has_webapp(self) -> bool: return os.path.isdir(self.webapp_dir) @property def node_modules_dir(self) -> str: return os.path.join(self.webapp_dir, 'node_modules') def are_node_modules_installed(self) -> bool: return os.path.isdir(self.node_modules_dir) @property def resources_dir(self) -> str: return os.path.join(self.app_dir, 'resources') def has_resources(self) -> bool: return os.path.isdir(self.resources_dir) @property def install_dir(self) -> str: return os.path.join(self.app_dir, 'install') def has_install(self) -> bool: return os.path.isdir(self.install_dir) @property def config_dir(self) -> str: return os.path.join(self.app_dir, 'config') def has_config(self) -> bool: return os.path.isdir(self.config_dir) @property def bootstrap_dir(self) -> str: return os.path.join(idea.props.project_source_dir, 'idea-bootstrap') def find_app_meta_file(self) -> str: src_dir = self.src_dir files = os.listdir(src_dir) for file in files: if file.endswith('_meta'): return os.path.join(src_dir, file, '__init__.py') raise idea.exceptions.build_failed(f'could not find app meta file (__init__.py) in: {src_dir}') def clean(self): if self.has_src(): src_dist = os.path.join(self.src_dir, 'dist') if os.path.isdir(src_dist): idea.console.print(f'deleting {src_dist} ...') shutil.rmtree(src_dist, ignore_errors=True) egg_name = self.app_name.replace('-', '_') egg_info_name = f'{egg_name}.egg-info' src_egg = os.path.join(self.src_dir, egg_info_name) if os.path.isdir(src_egg): idea.console.print(f'deleting {src_egg} ...') shutil.rmtree(os.path.join(self.src_dir, egg_info_name), ignore_errors=True) if self.has_webapp(): skip_web = os.environ.get('IDEA_SKIP_WEB_BUILD', '0') if skip_web == '0': if os.path.isdir(self.webapp_build_dir): idea.console.print(f'deleting {self.webapp_build_dir} ...') shutil.rmtree(self.webapp_build_dir, ignore_errors=True) if os.path.isdir(self.output_dir): idea.console.print(f'deleting {self.output_dir} ...') shutil.rmtree(self.output_dir) if os.path.isfile(self.output_archive_file): idea.console.print(f'deleting {self.output_archive_file} ...') os.remove(self.output_archive_file) if self.app_name == 'idea-administrator' or self.app_name == 'ad-sync': files = os.listdir(idea.props.deployment_administrator_dir) for file in files: if file == 'Dockerfile' or file == 'cfn_params_2_values.sh' or file == 'python.sh': continue file_path = os.path.join(idea.props.deployment_administrator_dir, file) if os.path.isfile(file_path): idea.console.print(f'deleting {file_path} ...') os.remove(os.path.join(idea.props.deployment_administrator_dir, file)) elif os.path.isdir(file_path): idea.console.print(f'deleting {file_path} ...') shutil.rmtree(file_path) def pre_build_src(self): if not self.has_src(): return PythonAppMetaFileUpdater(meta_file=self.find_app_meta_file()).update() def build_src(self): if not self.has_src(): return with self.c.cd(self.src_dir): self.c.run(f'{idea.utils.idea_python} setup.py sdist') def pre_build_webapp(self): if not self.has_webapp(): return webapp_dir = self.webapp_dir app_name = self.app_name app_version = self.app_version release_version = self.release_version NpmPackageJsonFileUpdater( package_json_file=os.path.join(webapp_dir, 'package.json'), app_name=app_name, app_version=app_version, release_version=release_version ).update() WebAppEnvFileUpdater( webapp_env_file=os.path.join(webapp_dir, '.env'), app_name=app_name, app_version=app_version, release_version=release_version ).update() def build_webapp(self): skip_web = os.environ.get('IDEA_SKIP_WEB_BUILD', '0') if skip_web == '1': return if not self.has_webapp(): return with self.c.cd(self.webapp_dir): self.c.run('yarn install && yarn build') def copy_build_outputs(self): output_dir = self.output_dir shutil.rmtree(output_dir, ignore_errors=True) os.makedirs(output_dir, exist_ok=True) # src (sdist) if self.has_src(): app_name = self.app_name # python does not accept server and does some funky normalization on the semver. # this is only applicable for pre-releases or dev branches. e.g. 3.0.0-dev.1 gets converted to 3.0.0.dev1 normalized_python_app_version = idea.utils.get_package_meta(self.c, self.src_dir, 'version') sdist_name = f'{app_name}-{normalized_python_app_version}.tar.gz' sdist = os.path.join(self.src_dir, 'dist', sdist_name) shutil.copy(sdist, os.path.join(output_dir, f'{app_name}-lib.tar.gz')) # webapp if self.has_webapp(): shutil.copytree(self.webapp_build_dir, os.path.join(output_dir, 'webapp')) # config if self.has_config(): shutil.copytree(self.config_dir, os.path.join(output_dir, 'config')) # resources if self.has_resources(): shutil.copytree(self.resources_dir, os.path.join(output_dir, 'resources')) shutil.copytree(self.bootstrap_dir, os.path.join(output_dir, 'resources', 'bootstrap')) def build(self): idea.console.print_header_block(f'build {self.app_name}') self.pre_build_src() self.build_src() self.pre_build_webapp() self.build_webapp() # copy build outputs to project build dir self.copy_build_outputs()