diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c8dbce9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,35 @@ +name: Build artifacts + +on: + push: + pull_request: + create: + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Checkout angle-builder + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + + - name: Install angle-builder + run: pip install . + + - name: Build artifacts for macos-x64 + run: angle-builder macos-x64 --artifact-output-folder /angle-builder-output + + - name: Build artifacts for macos-arm64 + run: angle-builder macos-arm64 --artifact-output-folder /angle-builder-output + + - name: Build artifacts for macos-universal + run: angle-builder macos-universal --artifact-output-folder /angle-builder-output + + - name: Store artifacts + uses: actions/upload-artifact@v4 + with: + name: macos-angle-artifacts + path: --artifact-output-folder /angle-builder-output diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e9e1e9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Temporary and binary files +*~ +*.py[cod] +*.so +*.cfg +!.isort.cfg +!setup.cfg +*.orig +*.log +*.pot +__pycache__/* +.cache/* +.*.swp +*/.ipynb_checkpoints/* +.DS_Store + +# Project files +.ropeproject +.project +.pydevproject +.settings +.idea +.vscode +tags + +# Package files +*.egg +*.eggs/ +.installed.cfg +*.egg-info + +# Unittest and coverage +htmlcov/* +.coverage +.coverage.* +.tox +junit*.xml +coverage.xml +.pytest_cache/ + +# Build and docs folder/files +build/* +dist/* +sdist/* +docs/api/* +docs/_rst/* +docs/_build/* +cover/* +MANIFEST + +# Per-project virtualenvs +.venv*/ +.conda*/ +.python-version diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7c7f513 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,7 @@ +In the interest of fostering an open and welcoming community, we as +contributors and maintainers need to ensure participation in our project and +our sister projects is a harassment-free and positive experience for everyone. +It is vital that all interaction is conducted in a manner conveying respect, +open-mindedness and gratitude. + +Please consult the [latest Kivy Code of Conduct](https://github.com/kivy/kivy/blob/master/CODE_OF_CONDUCT.md). diff --git a/CONTACT.md b/CONTACT.md new file mode 100644 index 0000000..1c0ea63 --- /dev/null +++ b/CONTACT.md @@ -0,0 +1,6 @@ +# Contacting the Kivy Team + +If you are looking to contact the Kivy Team (who are responsible for managing +the angle-builder project), including looking for support, please see our +latest [Contact Us](https://github.com/kivy/kivy/blob/master/CONTACT.md) +document. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2a6b617 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contribution Guidelines + +angle-builder is part of the [Kivy](https://kivy.org) ecosystem - a large group of +products used by many thousands of developers for free, but it +is built entirely by the contributions of volunteers. We welcome (and rely on) +users who want to give back to the community by contributing to the project. + +Contributions can come in many forms. See the latest +[Contribution Guidelines](https://github.com/kivy/kivy/blob/master/CONTRIBUTING.md) +for how you can help us. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29e3ea1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2023 Kivy Team and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index ba4a099..fd67fc4 100644 --- a/README.md +++ b/README.md @@ -1 +1,139 @@ -# angle-builder \ No newline at end of file +# angle-builder + +angle-builder is a dedicated tool designed to streamline the build process of the ANGLE library, +a critical component utilized by the Kivy project. + +[![Backers on Open Collective](https://opencollective.com/kivy/backers/badge.svg)](#backers) +[![Sponsors on Open Collective](https://opencollective.com/kivy/sponsors/badge.svg)](#sponsors) + +## Why angle-builder? + +Building the ANGLE library manually can be a time-consuming and resource-intensive task, +demanding substantial disk space and several minutes on a performant laptop. + +Recognizing these challenges, we developed `angle-builder` mainly with CI/CD in mind, even though +it can be used on your own development machine. + +## Key features + +- **Simplified Build Process**: Our tool automates the intricate steps involved in building the ANGLE library, making it easy for developers to integrate into their workflow. +- **Binaries Consolidation for Apple Platforms**: `angle-builder` includes scripts to merge the generated binaries into a single one for Apple platforms, when applicable. This results in a fat .dylib for macOS and an .xcframework for iOS for `-universal` targets. +- **CI/CD Integration**: We use GitHub Actions to build the library and store the binaries for later use. This allows us to avoid the time-consuming and resource-intensive build process on each CI/CD run, or on each developer's machine. + +## Installation + +Clone the repository and install the package: +``` +$ git clone https://github.com/kivy/angle-builder.git +$ cd angle-builder +$ python3 -m venv venv +$ source venv/bin/activate +$ pip install . +``` + +## Usage + +### Building ANGLE via ``angle-builder`` + +The following targets are available: +- `macos-x64`: Creates a zip file containing `libEGL.dylib`, `libGLESv2.dylib`, the `include` folder, and the `LICENSE` file for Intel-based macs. +- `macos-arm64`: Creates a zip file containing `libEGL.dylib`, `libGLESv2.dylib`, the `include` folder, and the `LICENSE` file for Apple Silicon macs. +- `macos-universal`: Creates a zip file containing `libEGL.dylib`, `libGLESv2.dylib`, the `include` folder, and the `LICENSE` file for Intel-based and Apple Silicon macs, by merging the binaries generated by the `macos-x64` and `macos-arm64` targets. + + +To build the library, run the following command: +``` +$ angle-builder +``` + +For the list of all the available options, run: +``` +$ angle-builder -h +``` + + +## License + +angle-builder is [MIT licensed](LICENSE), actively developed by a great +community and is supported by many projects managed by the +[Kivy Organization](https://www.kivy.org/about.html). + +## Documentation + +[Documentation for this repository](). + +## Support + +Are you having trouble using angle-builder? +Is there an error you don’t understand? Are you trying to figure out how to use +it? We have volunteers who can help! + +The best channels to contact us for support are listed in the latest +[Contact Us](https://github.com/kivy/angle-builder/blob/master/CONTACT.md) document. + +## Contributing + +angle-builder is part of the [Kivy](https://kivy.org) ecosystem - a large group of +products used by many thousands of developers for free, but it +is built entirely by the contributions of volunteers. We welcome (and rely on) +users who want to give back to the community by contributing to the project. + +Contributions can come in many forms. See the latest +[Contribution Guidelines](https://github.com/kivy/angle-builder/blob/master/CONTRIBUTING.md) +for how you can help us. + +## Code of Conduct + +In the interest of fostering an open and welcoming community, we as +contributors and maintainers need to ensure participation in our project and +our sister projects is a harassment-free and positive experience for everyone. +It is vital that all interaction is conducted in a manner conveying respect, +open-mindedness and gratitude. + +Please consult the [latest Code of Conduct](https://github.com/kivy/angle-builder/blob/master/CODE_OF_CONDUCT.md). + +## Contributors + +This project exists thanks to +[all the people who contribute](https://github.com/kivy/angle-builder/graphs/contributors). +[[Become a contributor](CONTRIBUTING.md)]. + + + +## Backers + +Thank you to [all of our backers](https://opencollective.com/kivy)! +🙏 [[Become a backer](https://opencollective.com/kivy#backer)] + + + +## Sponsors + +Special thanks to +[all of our sponsors, past and present](https://opencollective.com/kivy). +Support this project by +[[becoming a sponsor](https://opencollective.com/kivy#sponsor)]. + +Here are our top current sponsors. Please click through to see their websites, +and support them as they support us. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..88e2255 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=46.1.0"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b4678bd --- /dev/null +++ b/setup.cfg @@ -0,0 +1,69 @@ +[metadata] +name = angle-builder +description = Build and releases ANGLE binaries for different platforms +author = Kivy Team and other contributors +author_email = team@kivy.org +license = MIT +license_files = LICENSE +long_description = file: README.md +url = https://github.com/kivy/angle-builder +project_urls = + Source = https://github.com/kivy/angle-builder/ + + +platforms = any + +classifiers = + Development Status :: 4 - Beta + Programming Language :: Python + Topic :: Software Development :: Build Tools + +[options] +zip_safe = False +packages = find_namespace: +include_package_data = True +package_dir = + =src + +python_requires = >=3.8 + +# install_requires = + +[options.packages.find] +where = src +exclude = + tests + +[options.extras_require] +dev = + setuptools + pytest + pytest-cov + +[options.entry_points] +console_scripts = + angle-builder = angle_builder.builder:run + +[tool:pytest] +addopts = + --cov angle_builder --cov-report term-missing + --verbose +norecursedirs = + dist + build + .tox +testpaths = tests + + +[flake8] +# Some sane defaults for the code style checker flake8 +max_line_length = 88 +extend_ignore = E203, W503 +# ^ Black-compatible +# E203 and W503 have edge cases handled by black +exclude = + .tox + build + dist + .eggs + docs/conf.py \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b03de1a --- /dev/null +++ b/setup.py @@ -0,0 +1,8 @@ +""" + Setup file for angle-builder. + Use setup.cfg to configure the project. +""" +from setuptools import setup + +if __name__ == "__main__": + setup() diff --git a/src/angle_builder/__init__.py b/src/angle_builder/__init__.py new file mode 100644 index 0000000..7760638 --- /dev/null +++ b/src/angle_builder/__init__.py @@ -0,0 +1,9 @@ +from importlib.metadata import PackageNotFoundError, version + +try: + dist_name = "angle-builder" + __version__ = version(dist_name) +except PackageNotFoundError: # pragma: no cover + __version__ = "unknown" +finally: + del version, PackageNotFoundError diff --git a/src/angle_builder/angle.py b/src/angle_builder/angle.py new file mode 100644 index 0000000..02ad470 --- /dev/null +++ b/src/angle_builder/angle.py @@ -0,0 +1,368 @@ +import logging +import os +import subprocess +import shutil +import tempfile + +from angle_builder.storage import StorageFolderManager + + +class ANGLE: + def __init__( + self, + branch: str, + storage_manager: StorageFolderManager, + revision: str = None, + logger_level: int = logging.INFO, + ): + self.branch = branch + self.revision = revision + self.storage_manager = storage_manager + self._logger = logging.getLogger(__name__) + self._logger.setLevel(logger_level) + + @property + def underlined_branch(self) -> str: + """ + Returns the branch name with underscores instead of dots or slashes. + """ + return self.branch.replace(".", "_").replace("/", "__") + + @property + def angle_path(self) -> os.PathLike: + """ + Returns the path to our angle folder. + """ + return os.path.join( + self.storage_manager.folder_path, f"angle-{self.underlined_branch}" + ) + + def _clone_angle(self): + """ + Clone ANGLE repository into our storage folder. + (in a cross-platform way, without using sh.git, as it is not available on Windows) + """ + + subprocess.run( + [ + "git", + "clone", + "--branch", + self.branch, + "--single-branch", + "https://github.com/google/angle", + self.angle_path, + ], + cwd=self.storage_manager.folder_path, + check=True, + ) + + def _checkout_revision(self): + """ + Checkout the specified revision. + """ + + if self.revision is not None: + subprocess.run( + ["git", "checkout", self.revision], + cwd=self.angle_path, + check=True, + ) + + def clone_and_checkout(self) -> None: + """ + Clone ANGLE repository and checkout the specified revision. + """ + + self.storage_manager.ensure_folder() + + self._logger.info( + "Cloning (if needed) ANGLE repository for branch %s", self.branch + ) + + if not os.path.exists(self.angle_path): + self._clone_angle() + + self._checkout_revision() + + def bootstrap(self) -> None: + """ + Bootstrap ANGLE. + """ + self._logger.info( + "Bootstrapping ANGLE repository for branch %s", self.branch + ) + + subprocess.run( + ["python", "scripts/bootstrap.py"], + cwd=self.angle_path, + check=True, + ) + + def sync(self) -> None: + """ + Sync ANGLE. + """ + self._logger.info("Syncing ANGLE repository for branch %s", self.branch) + + subprocess.run( + ["gclient", "sync"], + cwd=self.angle_path, + check=True, + ) + + def _generate_build_targets(self, output_artifact_mode: str) -> None: + self._logger.info( + "Generating build targets for branch %s and output_artifact_mode %s", + self.branch, + output_artifact_mode, + ) + + builds = [] + + common_gn_args = [ + "is_component_build=false", + "is_debug=false", + ] + + if output_artifact_mode in ("macos-arm64", "macos-universal"): + builds.append( + { + "name": "macos-arm64", + "gn_args": common_gn_args + + [ + 'target_cpu="arm64"', + 'target_os="mac"', + ], + } + ) + + if output_artifact_mode in ("macos-x64", "macos-universal"): + builds.append( + { + "name": "macos-x64", + "gn_args": common_gn_args + + [ + 'target_cpu="x64"', + 'target_os="mac"', + ], + } + ) + + if output_artifact_mode in ( + "iphoneos-arm64", + "iphone*-universal", + ): + builds.append( + { + "name": "iphoneos-arm64", + "gn_args": common_gn_args + + [ + 'target_cpu="arm64"', + 'target_os="ios"', + "ios_enable_code_signing=false", + ], + } + ) + + if output_artifact_mode in ( + "iphonesimulator-x64", + "iphone*-universal", + ): + builds.append( + { + "name": "iphonesimulator-x64", + "gn_args": common_gn_args + + [ + 'target_cpu="x64"', + 'target_os="ios"', + "ios_enable_code_signing=false", + ], + } + ) + + if output_artifact_mode in ( + "iphonesimulator-arm64", + "iphone*-universal", + ): + builds.append( + { + "name": "iphonesimulator-arm64", + "gn_args": common_gn_args + + [ + 'target_cpu="arm64"', + 'target_os="ios"', + "ios_enable_code_signing=false", + ], + } + ) + + return builds + + def _gn_gen(self, build_target: dict) -> None: + """ + Run gn gen for the specified build target. + """ + + self._logger.info( + "Running gn gen for branch %s and build target %s", + self.branch, + build_target["name"], + ) + + subprocess.run( + [ + "gn", + "gen", + f"out/{build_target['name']}", + "--args=" + " ".join(build_target["gn_args"]), + ], + cwd=self.angle_path, + check=True, + ) + + def _autoninja_build(self, build_target: dict) -> None: + """ + Run autoninja for the specified build target. + """ + + self._logger.info( + "Running autoninja for branch %s and build target %s", + self.branch, + build_target["name"], + ) + + subprocess.run( + [ + "autoninja", + "-C", + f"out/{build_target['name']}", + "libEGL", + "libGLESv2", + ], + cwd=self.angle_path, + check=True, + ) + + def _create_macos_dylibs(self, output_artifact_mode: str) -> list: + libEGL_dylib_path = os.path.join( + self.angle_path, "out", output_artifact_mode, "libEGL.dylib" + ) + libGLESv2_dylib_path = os.path.join( + self.angle_path, "out", output_artifact_mode, "libGLESv2.dylib" + ) + + self._logger.info( + "Creating macos dylibs for branch %s and build target %s", + self.branch, + output_artifact_mode, + ) + + if output_artifact_mode == "macos-universal": + # Ensure the fake macos-universal folder exists (if exists, clean it up) + macos_universal_folder = os.path.join( + self.angle_path, "out", "macos-universal" + ) + if os.path.exists(macos_universal_folder): + shutil.rmtree(macos_universal_folder) + os.makedirs(macos_universal_folder) + + self._logger.info( + "Lipo-ing macos dylibs for branch %s and build target %s", + self.branch, + output_artifact_mode, + ) + + # merge x64 and arm64 dylibs into fat dylibs + subprocess.run( + [ + "lipo", + "-create", + os.path.join( + self.angle_path, "out", "macos-x64", "libEGL.dylib" + ), + os.path.join( + self.angle_path, "out", "macos-arm64", "libEGL.dylib" + ), + "-output", + libEGL_dylib_path, + ], + cwd=self.angle_path, + check=True, + ) + + subprocess.run( + [ + "lipo", + "-create", + os.path.join( + self.angle_path, "out", "macos-x64", "libGLESv2.dylib" + ), + os.path.join( + self.angle_path, "out", "macos-arm64", "libGLESv2.dylib" + ), + "-output", + libGLESv2_dylib_path, + ], + cwd=self.angle_path, + check=True, + ) + + return [libEGL_dylib_path, libGLESv2_dylib_path] + + def build(self, output_artifact_mode: str, output_folder: str) -> None: + """ + Build ANGLE with the specified output output_artifact_mode. + + The output_artifact_mode can be one of: + - macos-arm64 (produces a zip with arm64 dylibs, include folder and LICENSE) + - macos-x64 (produces a zip with x64 dylibs, include folder and LICENSE) + - macos-universal (produces a zip with fat dylibs, include folder and LICENSE) + - iphoneos-arm64 (produces a zip with iphoneos-arm64 .Framework, include folder and LICENSE) + - iphonesimulator-x64 (produces a zip with iphonesimulator-x64 .Framework, include folder and LICENSE) + - iphonesimulator-arm64 (produces a zip with iphonesimulator-arm64 .Framework, include folder and LICENSE) + - iphone*-universal (produces a zip with a .xcframework that contains iphoneos-arm64, iphonesimulator-x64 and iphonesimulator-arm64 .Frameworks, include folder and LICENSE) + """ + self.clone_and_checkout() + self.bootstrap() + self.sync() + + build_targets = self._generate_build_targets(output_artifact_mode) + + for build_target in build_targets: + self._gn_gen(build_target) + + for build_target in build_targets: + self._autoninja_build(build_target) + + include_folder_path = os.path.join(self.angle_path, "include") + license_path = os.path.join(self.angle_path, "LICENSE") + + if output_artifact_mode.startswith("macos"): + libs = self._create_macos_dylibs(output_artifact_mode) + + with tempfile.TemporaryDirectory() as temp_dir: + self._logger.info( + "Creating zip for branch %s and output_artifact_mode %s", + self.branch, + output_artifact_mode, + ) + # Copy libs, include folder and LICENSE to temp_dir + for lib in libs: + shutil.copy(lib, temp_dir) + + shutil.copytree( + include_folder_path, os.path.join(temp_dir, "include") + ) + shutil.copy(license_path, temp_dir) + + # Create a zip file with libs, include folder and LICENSE + shutil.make_archive( + os.path.join( + output_folder, + f"angle-{self.underlined_branch}-{output_artifact_mode}", + ), + "zip", + root_dir=temp_dir, + verbose=True, + ) diff --git a/src/angle_builder/builder.py b/src/angle_builder/builder.py new file mode 100644 index 0000000..51f8fc1 --- /dev/null +++ b/src/angle_builder/builder.py @@ -0,0 +1,121 @@ +import argparse +import logging +import os +import sys + +from angle_builder import __version__ +from angle_builder.depot_tools import DepotTools +from angle_builder.storage import StorageFolderManager +from angle_builder.angle import ANGLE + +__author__ = "Kivy Team and other contributors" +__copyright__ = "Kivy Team and other contributors" +__license__ = "MIT" + + +def parse_args(args): + """Parse command line parameters + + Args: + args (List[str]): command line parameters as list of strings + (for example ``["--help"]``). + + Returns: + :obj:`argparse.Namespace`: command line parameters namespace + """ + parser = argparse.ArgumentParser(description="ANGLE builder CLI") + + parser.add_argument( + dest="output_artifact_mode", + help="Output artifact mode", + ) + parser.add_argument( + "--version", + action="version", + version=f"angle-builder {__version__}", + ) + parser.add_argument( + "-v", + "--verbose", + dest="loglevel", + help="set loglevel to INFO", + action="store_const", + const=logging.INFO, + ) + parser.add_argument( + "-vv", + "--very-verbose", + dest="loglevel", + help="set loglevel to DEBUG", + action="store_const", + const=logging.DEBUG, + ) + parser.add_argument( + "--branch", + dest="branch", + help="ANGLE branch to build", + default="chromium/6045", + ) + parser.add_argument( + "--storage-folder", + dest="storage_folder", + help="Storage folder for ANGLE build", + default=None, + ) + parser.add_argument( + "--artifact-output-folder", + dest="artifact_output_folder", + help="Output folder for artifacts", + default=None, + ) + + parser.set_defaults(loglevel=logging.INFO) + + return parser.parse_args(args) + + +def main(args): + """ + Args: + args (List[str]): command line parameters as list of strings + (for example ``["--verbose", "42"]``). + """ + args = parse_args(args) + + if args.artifact_output_folder is None: + args.artifact_output_folder = os.path.join( + os.getcwd(), "angle-artifacts" + ) + + storage_manager = StorageFolderManager( + user_path=args.storage_folder, + logger_level=args.loglevel, + ) + storage_manager.ensure_folder() + + depot_tools = DepotTools( + storage_manager=storage_manager, logger_level=args.loglevel + ) + depot_tools.ensure_depot_tools() + + angle = ANGLE( + branch=args.branch, + storage_manager=storage_manager, + logger_level=args.loglevel, + ) + angle.build( + output_artifact_mode=args.output_artifact_mode, + output_folder=args.artifact_output_folder, + ) + + +def run(): + """Calls :func:`main` passing the CLI arguments extracted from :obj:`sys.argv` + + This function can be used as entry point to create console scripts with setuptools. + """ + main(sys.argv[1:]) + + +if __name__ == "__main__": + run() diff --git a/src/angle_builder/depot_tools.py b/src/angle_builder/depot_tools.py new file mode 100644 index 0000000..46d8c96 --- /dev/null +++ b/src/angle_builder/depot_tools.py @@ -0,0 +1,65 @@ +import logging +import os +import subprocess + +from angle_builder.storage import StorageFolderManager + + +class DepotTools: + def __init__( + self, + storage_manager: StorageFolderManager, + logger_level: int = logging.INFO, + ): + """ + Args: + storage_manager (StorageFolderManager): Storage folder manager. + logger_level (int): Logging level. + """ + self.storage_manager = storage_manager + self._logger = logging.getLogger(__name__) + self._logger.setLevel(logger_level) + + @property + def depot_tools_path(self) -> os.PathLike: + """ + Returns the path to our depot_tools folder. + """ + return os.path.join(self.storage_manager.folder_path, "depot_tools") + + def _clone_darwin_linux(self): + """ + Clones depot_tools into our storage folder for Linux and macOS. + """ + self._logger.info( + "Cloning depot_tools into '%s'", self.depot_tools_path + ) + + subprocess.run( + [ + "git", + "clone", + "https://chromium.googlesource.com/chromium/tools/depot_tools.git", + self.depot_tools_path, + ], + check=True, + ) + + def _ensure_depot_tools_is_in_path(self): + _path_content = os.environ["PATH"].split(os.pathsep) + + if self.depot_tools_path not in _path_content: + os.environ["PATH"] = os.pathsep.join( + [self.depot_tools_path, os.environ["PATH"]] + ) + + def ensure_depot_tools(self) -> None: + """ + Ensures that depot_tools is downloaded and in the path. + """ + self._logger.info("Ensuring depot_tools is available and in PATH") + + if not os.path.exists(self.depot_tools_path): + self._clone_darwin_linux() + + self._ensure_depot_tools_is_in_path() diff --git a/src/angle_builder/storage.py b/src/angle_builder/storage.py new file mode 100644 index 0000000..6f5e90c --- /dev/null +++ b/src/angle_builder/storage.py @@ -0,0 +1,65 @@ +import logging +import os + + +class StorageFolderManager: + """ + Ensures that the storage folder exists, and provides methods + to control the lifecycle of the storage folder. + """ + + def __init__(self, user_path: str = None, logger_level: int = logging.INFO): + """ + Args: + user_path (str): Path to the storage folder, if None, it will be + created in the user's home directory. + logger_level (int): Logging level. + """ + self.storage_folder_name = ".angle-builder" + + self._logger = logging.getLogger(__name__) + self._logger.setLevel(logger_level) + + if user_path is None: + self.folder_path = os.path.join( + os.path.expanduser("~"), self.storage_folder_name + ) + else: + self.folder_path = os.path.join(user_path, self.storage_folder_name) + + def ensure_folder(self) -> None: + """ + Ensures that the build folder exists. + If it does not exist, it creates it. + """ + if not os.path.exists(self.folder_path): + os.makedirs(self.folder_path) + self._logger.info( + "Build folder '%s' created at '%s'", + self.storage_folder_name, + self.folder_path, + ) + else: + self._logger.info( + "Build folder '%s' already exists at '%s'", + self.storage_folder_name, + self.folder_path, + ) + + def delete_folder(self) -> None: + """ + Deletes the build folder. + """ + if os.path.exists(self.folder_path): + os.rmdir(self.folder_path) + self._logger.info( + "Build folder '%s' deleted from '%s'", + self.storage_folder_name, + self.folder_path, + ) + else: + self._logger.info( + "Build folder '%s' does not exist at '%s'", + self.storage_folder_name, + self.folder_path, + )