From 0a626105e92a4f41632b26023206d0f71edc0c28 Mon Sep 17 00:00:00 2001 From: Jason Shepherd Date: Tue, 9 Apr 2024 12:47:06 +1000 Subject: [PATCH 1/2] generate requirements for prefetch --- Dockerfile-build | 21 ++++ pip_find_builddeps.py | 236 +++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 6 ++ requirements-build.in | 8 ++ requirements-build.txt | 44 ++++++++ requirements.txt | 103 +++++++++++++++++- 6 files changed, 417 insertions(+), 1 deletion(-) create mode 100644 Dockerfile-build create mode 100755 pip_find_builddeps.py create mode 100644 pyproject.toml create mode 100644 requirements-build.in create mode 100644 requirements-build.txt diff --git a/Dockerfile-build b/Dockerfile-build new file mode 100644 index 0000000000..eb91af0d95 --- /dev/null +++ b/Dockerfile-build @@ -0,0 +1,21 @@ +FROM registry.access.redhat.com/ubi9/python-39:1-117.1684741281 + +# Set the working directory in the container +WORKDIR /projects + +USER 0 +ADD . . +RUN chown -R 1001:0 ./ +USER 1001 + +RUN pip install pip-tools + +RUN pip-compile pyproject.toml --generate-hashes + +RUN cat requirements.txt + +RUN ./pip_find_builddeps.py requirements.txt -o requirements-build.in + +RUN pip-compile requirements-build.in --allow-unsafe --generate-hashes + +RUN cat requirements-build.txt \ No newline at end of file diff --git a/pip_find_builddeps.py b/pip_find_builddeps.py new file mode 100755 index 0000000000..e6688ffb79 --- /dev/null +++ b/pip_find_builddeps.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +import argparse +import datetime +import logging +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +SCRIPT_NAME = Path(sys.argv[0]).name + +DESCRIPTION = """\ +Find build dependencies for all your runtime dependencies. The input to this +script must be a requirements.txt file containing all the *recursive* runtime +dependencies. You can use pip-compile to generate such a file. The output is an +intermediate file that must first go through pip-compile before being used in +a Cachito request. +""" + +logging.basicConfig(format="%(levelname)s: %(message)s") + +log = logging.getLogger(__name__) +log.setLevel(logging.INFO) + + +class FindBuilddepsError(Exception): + """Failed to find build dependencies.""" + + +def _pip_download(requirements_files, output_file, tmpdir, no_cache, allow_binary): + """Run pip download, write output to file.""" + cmd = [ + "pip", + "download", + "-d", + tmpdir, + "--no-binary", + ":all:", + "--use-pep517", + "--verbose", + ] + if allow_binary: + cmd.remove("--no-binary") + cmd.remove(":all:") + if no_cache: + cmd.append("--no-cache-dir") + for file in requirements_files: + cmd.append("-r") + cmd.append(file) + + with open(output_file, "w") as outfile: + subprocess.run(cmd, stdout=outfile, stderr=outfile, check=True) + + +def _filter_builddeps(pip_download_output_file): + """Find builddeps in output of pip download.""" + # Requirement is a sequence of non-whitespace, non-';' characters + # Example: package, package==1.0, package[extra]==1.0 + requirement_re = r"[^\s;]+" + # Leading whitespace => requirement is a build dependency + # (because all recursive runtime dependencies were present in input files) + builddep_re = re.compile(rf"^\s+Collecting ({requirement_re})") + + with open(pip_download_output_file) as f: + matches = (builddep_re.match(line) for line in f) + builddeps = set(match.group(1) for match in matches if match) + + return sorted(builddeps) + + +def find_builddeps( + requirements_files, no_cache=False, ignore_errors=False, allow_binary=False +): + """ + Find build dependencies for packages in requirements files. + + :param requirements_files: list of requirements file paths + :param no_cache: do not use pip cache when downloading packages + :param ignore_errors: generate partial output even if pip download fails + :return: list of build dependencies and bool whether output is partial + """ + tmpdir = tempfile.mkdtemp(prefix=f"{SCRIPT_NAME}-") + pip_output_file = Path(tmpdir) / "pip-download-output.txt" + is_partial = False + + try: + log.info("Running pip download, this may take a while") + _pip_download( + requirements_files, pip_output_file, tmpdir, no_cache, allow_binary + ) + except subprocess.CalledProcessError: + msg = f"Pip download failed, see {pip_output_file} for more info" + if ignore_errors: + log.error(msg) + log.warning("Ignoring error...") + is_partial = True + else: + raise FindBuilddepsError(msg) + + log.info("Looking for build dependencies in the output of pip download") + builddeps = _filter_builddeps(pip_output_file) + + # Remove tmpdir only if pip download was successful + if not is_partial: + shutil.rmtree(tmpdir) + + return builddeps, is_partial + + +def generate_file_content(builddeps, is_partial): + """ + Generate content to write to output file. + + :param builddeps: list of build dependencies to include in file + :param is_partial: indicates that list of build dependencies may be partial + :return: file content + """ + # Month Day Year HH:MM:SS + date = datetime.datetime.now().strftime("%b %d %Y %H:%M:%S") + + lines = [f"# Generated by {SCRIPT_NAME} on {date}"] + if builddeps: + lines.extend(builddeps) + else: + lines.append("# ") + + if is_partial: + lines.append("# ") + + file_content = "\n".join(lines) + return file_content + + +def _parse_requirements_file(builddeps_file): + """Find deps requirements-build.in file.""" + try: + with open(builddeps_file) as f: + # ignore line comments or comments added after dependency is declared + requirement_re = re.compile(r"^([^\s#;]+)") + matches = (requirement_re.match(line) for line in f) + return set(match.group(1) for match in matches if match) + except FileNotFoundError: + # it's ok if the file doens't exist. + return set() + + +def _sanity_check_args(ap, args): + if args.only_write_on_update and not args.output_file: + ap.error("--only-write-on-update requires an output-file (-o/--output-file).") + + +def main(): + """Run script.""" + ap = argparse.ArgumentParser(description=DESCRIPTION) + ap.add_argument("requirements_files", metavar="REQUIREMENTS_FILE", nargs="+") + ap.add_argument( + "-o", "--output-file", metavar="FILE", help="write output to this file" + ) + ap.add_argument( + "-a", + "--append", + action="store_true", + help="append to output file instead of overwriting", + ) + ap.add_argument( + "--no-cache", + action="store_true", + help="do not use pip cache when downloading packages", + ) + ap.add_argument( + "--ignore-errors", + action="store_true", + help="generate partial output even if pip download fails", + ) + ap.add_argument( + "--only-write-on-update", + action="store_true", + help=( + "only write output file if dependencies will be modified - or new " + "dependencies will be added if used in conjunction with -a/--append." + ), + ) + ap.add_argument( + "--allow-binary", + action="store_true", + help=( + "do not find build dependencies for packages with wheels " + "available for the current platform" + ), + ) + + args = ap.parse_args() + _sanity_check_args(ap, args) + + log.info( + "Please make sure the input files meet the requirements of this script (see --help)" + ) + + builddeps, is_partial = find_builddeps( + args.requirements_files, + no_cache=args.no_cache, + ignore_errors=args.ignore_errors, + allow_binary=args.allow_binary, + ) + + if args.only_write_on_update: + original_builddeps = _parse_requirements_file(args.output_file) + if args.append: + # append only new dependencies + builddeps = sorted(set(builddeps) - original_builddeps) + if not builddeps or set(builddeps) == original_builddeps: + log.info("No new build dependencies found.") + return + + file_content = generate_file_content(builddeps, is_partial) + + log.info("Make sure to pip-compile the output before submitting a Cachito request") + if is_partial: + log.warning("Pip download failed, output may be incomplete!") + + if args.output_file: + mode = "a" if args.append else "w" + with open(args.output_file, mode) as f: + print(file_content, file=f) + else: + print(file_content) + + +if __name__ == "__main__": + try: + main() + except FindBuilddepsError as e: + log.error("%s", e) + exit(1) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..578af4b3a0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "devfile-sample-python-basic" +version = "0.1.0" +dependencies = [ + "Flask==2.3.3", +] diff --git a/requirements-build.in b/requirements-build.in new file mode 100644 index 0000000000..e6e728a52e --- /dev/null +++ b/requirements-build.in @@ -0,0 +1,8 @@ +# Generated by pip_find_builddeps.py on Apr 09 2024 12:17:44 +flit_core<4 +packaging>=20 +setuptools>=40.8.0 +setuptools>=56 +setuptools_scm[toml]>=3.4.1 +typing-extensions +wheel diff --git a/requirements-build.txt b/requirements-build.txt new file mode 100644 index 0000000000..5bde5ca032 --- /dev/null +++ b/requirements-build.txt @@ -0,0 +1,44 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --allow-unsafe --generate-hashes requirements-build.in +# +flit-core==3.9.0 \ + --hash=sha256:72ad266176c4a3fcfab5f2930d76896059851240570ce9a98733b658cb786eba \ + --hash=sha256:7aada352fb0c7f5538c4fafeddf314d3a6a92ee8e2b1de70482329e42de70301 + # via -r requirements-build.in +packaging==24.0 \ + --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ + --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 + # via + # -r requirements-build.in + # setuptools-scm +setuptools-scm==8.0.4 \ + --hash=sha256:b47844cd2a84b83b3187a5782c71128c28b4c94cad8bfb871da2784a5cb54c4f \ + --hash=sha256:b5f43ff6800669595193fd09891564ee9d1d7dcb196cab4b2506d53a2e1c95c7 + # via -r requirements-build.in +tomli==2.0.1 \ + --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ + --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f + # via + # -r requirements-build.in + # setuptools-scm +typing-extensions==4.11.0 \ + --hash=sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0 \ + --hash=sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a + # via + # -r requirements-build.in + # setuptools-scm +wheel==0.43.0 \ + --hash=sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85 \ + --hash=sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81 + # via -r requirements-build.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==69.2.0 \ + --hash=sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e \ + --hash=sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c + # via + # -r requirements-build.in + # setuptools-scm \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cc35792f29..11b58b70fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,102 @@ -Flask==2.3.3 \ No newline at end of file +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --generate-hashes pyproject.toml +# +blinker==1.7.0 \ + --hash=sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9 \ + --hash=sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182 + # via flask +click==8.1.7 \ + --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ + --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de + # via flask +flask==2.3.3 \ + --hash=sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc \ + --hash=sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b + # via devfile-sample-python-basic (pyproject.toml) +importlib-metadata==7.1.0 \ + --hash=sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570 \ + --hash=sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2 + # via flask +itsdangerous==2.1.2 \ + --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \ + --hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a + # via flask +jinja2==3.1.3 \ + --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ + --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 + # via flask +markupsafe==2.1.5 \ + --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ + --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ + --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ + --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ + --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ + --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ + --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ + --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ + --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ + --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ + --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ + --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ + --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ + --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ + --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ + --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ + --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ + --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ + --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ + --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ + --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ + --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ + --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ + --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ + --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ + --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ + --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ + --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ + --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ + --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ + --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ + --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ + --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ + --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ + --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ + --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ + --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ + --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ + --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ + --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ + --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ + --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ + --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ + --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ + --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ + --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ + --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ + --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ + --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ + --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ + --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ + --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ + --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ + --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ + --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ + --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ + --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ + --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ + --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ + --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 + # via + # jinja2 + # werkzeug +werkzeug==3.0.2 \ + --hash=sha256:3aac3f5da756f93030740bc235d3e09449efcf65f2f55e3602e1d851b8f48795 \ + --hash=sha256:e39b645a6ac92822588e7b39a692e7828724ceae0b0d702ef96701f90e70128d + # via flask +zipp==3.18.1 \ + --hash=sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b \ + --hash=sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715 + # via importlib-metadata \ No newline at end of file From 03beb8771041e95e45e5613d0fc0a08fdec229c7 Mon Sep 17 00:00:00 2001 From: Jason Shepherd Date: Tue, 9 Apr 2024 12:47:17 +1000 Subject: [PATCH 2/2] configure prefetch --- .tekton/devfile-sample-python-basic-fd5a-pull-request.yaml | 2 ++ .tekton/devfile-sample-python-basic-fd5a-push.yaml | 2 ++ README.md | 6 ++++++ 3 files changed, 10 insertions(+) diff --git a/.tekton/devfile-sample-python-basic-fd5a-pull-request.yaml b/.tekton/devfile-sample-python-basic-fd5a-pull-request.yaml index 0d13ba646e..09c7d51ade 100644 --- a/.tekton/devfile-sample-python-basic-fd5a-pull-request.yaml +++ b/.tekton/devfile-sample-python-basic-fd5a-pull-request.yaml @@ -32,6 +32,8 @@ spec: value: '{{revision}}' - name: hermetic value: "true" + - name: prefetch-input + value: '{"type": "pip", "path": "."}' pipelineSpec: finally: - name: show-sbom diff --git a/.tekton/devfile-sample-python-basic-fd5a-push.yaml b/.tekton/devfile-sample-python-basic-fd5a-push.yaml index 18b5f1b804..c8276cc5b1 100644 --- a/.tekton/devfile-sample-python-basic-fd5a-push.yaml +++ b/.tekton/devfile-sample-python-basic-fd5a-push.yaml @@ -29,6 +29,8 @@ spec: value: '{{revision}}' - name: hermetic value: "true" + - name: prefetch-input + value: '{"type": "pip", "path": "."}' pipelineSpec: finally: - name: show-sbom diff --git a/README.md b/README.md index 2573e0104c..f91baebee2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ Before you begin creating an application with this `devfile` code sample, it's h 3. The `devfile.yaml` [`kubernetes-deploy` component](https://github.com/devfile-samples/devfile-sample-python-basic/blob/main/devfile.yaml#L31-L43) points to a `deploy.yaml` file that contains instructions for deploying the built container image. 4. The `devfile.yaml` [`deploy` command](https://github.com/devfile-samples/devfile-sample-python-basic/blob/main/devfile.yaml#L51-L59) completes the [outerloop](https://devfile.io/docs/2.2.0/innerloop-vs-outerloop) deployment phase by pointing to the `image-build` and `kubernetes-deploy` components to create your application. +# Customizations + +1. Enabled hermetic builds in ./.tekton/*.yaml +2. Generating prefetch requirements.txt and requirements-build.txt files using Dockerfile-build +3. Enabled prefetch-input in ./.tekton/*.yaml + ### Additional resources * For more information about Python, see [Python](https://www.python.org/). * For more information about devfiles, see [Devfile.io](https://devfile.io/).