Skip to content

Commit fdfa9f3

Browse files
authored
chore: allow generation-input as folder that supplies config and vers… (#3700)
…ions file. This change adds `--generation-input` option to generation command, only intend to be used when `--generation-config-path` is not provided. If none of these 2 provided, then default behavior does not change. Also added a clean up to delete the temp output folder created in generation process. This should only be an improvement to local dev workflow.
1 parent 77aed97 commit fdfa9f3

File tree

4 files changed

+137
-6
lines changed

4 files changed

+137
-6
lines changed

hermetic_build/library_generation/cli/entry_point.py

+60-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import sys
1616
from typing import Optional
1717
import click as click
18+
import shutil
19+
from pathlib import Path
1820
from library_generation.generate_repo import generate_from_yaml
1921
from common.model.generation_config import from_yaml, GenerationConfig
2022

@@ -35,6 +37,22 @@ def main(ctx):
3537
help="""
3638
Absolute or relative path to a generation_config.yaml that contains the
3739
metadata about library generation.
40+
When not set, will use generation_config.yaml from --generation-input.
41+
When neither this or --generation-input is set default to
42+
generation_config.yaml in the current working directory
43+
""",
44+
)
45+
@click.option(
46+
"--generation-input",
47+
required=False,
48+
default=None,
49+
type=str,
50+
help="""
51+
Absolute or relative path to a input folder that contains
52+
generation_config.yaml and versions.txt.
53+
This is only used when --generation-config-path is not set.
54+
When neither this or --generation-config-path is set default to
55+
generation_config.yaml in the current working directory
3856
""",
3957
)
4058
@click.option(
@@ -75,6 +93,7 @@ def main(ctx):
7593
)
7694
def generate(
7795
generation_config_path: Optional[str],
96+
generation_input: Optional[str],
7897
library_names: Optional[str],
7998
repository_path: str,
8099
api_definitions_path: str,
@@ -95,6 +114,7 @@ def generate(
95114
"""
96115
__generate_repo_impl(
97116
generation_config_path=generation_config_path,
117+
generation_input=generation_input,
98118
library_names=library_names,
99119
repository_path=repository_path,
100120
api_definitions_path=api_definitions_path,
@@ -106,15 +126,28 @@ def __generate_repo_impl(
106126
library_names: Optional[str],
107127
repository_path: str,
108128
api_definitions_path: str,
129+
generation_input: Optional[str],
109130
):
110131
"""
111132
Implementation method for generate().
112133
The decoupling of generate and __generate_repo_impl is
113134
meant to allow testing of this implementation function.
114135
"""
115136

137+
# only use generation_input when generation_config_path is not provided and
138+
# generation_input provided. generation_config_path should be deprecated after
139+
# migration to 1pp.
116140
default_generation_config_path = f"{os.getcwd()}/generation_config.yaml"
117-
if generation_config_path is None:
141+
if generation_config_path is None and generation_input is not None:
142+
print(
143+
"generation_config_path is not provided, using generation-input folder provided"
144+
)
145+
generation_config_path = f"{generation_input}/generation_config.yaml"
146+
# copy versions.txt from generation_input to repository_path
147+
# override if present.
148+
_copy_versions_file(generation_input, repository_path)
149+
if generation_config_path is None and generation_input is None:
150+
print("Using default generation config path")
118151
generation_config_path = default_generation_config_path
119152
generation_config_path = os.path.abspath(generation_config_path)
120153
if not os.path.isfile(generation_config_path):
@@ -135,6 +168,32 @@ def __generate_repo_impl(
135168
)
136169

137170

171+
def _copy_versions_file(generation_input_path, repository_path):
172+
"""
173+
Copies the versions.txt file from the generation_input folder to the repository_path.
174+
Overrides the destination file if it already exists.
175+
176+
Args:
177+
generation_input_path (str): The path to the generation_input folder.
178+
repository_path (str): The path to the repository folder.
179+
"""
180+
source_file = Path(generation_input_path) / "versions.txt"
181+
destination_file = Path(repository_path) / "versions.txt"
182+
183+
if not source_file.exists():
184+
destination_file.touch()
185+
print(
186+
f"generation-input does not contain versions.txt. "
187+
f"Created empty versions file: {source_file}"
188+
)
189+
return
190+
try:
191+
shutil.copy2(source_file, destination_file)
192+
print(f"Copied '{source_file}' to '{destination_file}'")
193+
except Exception as e:
194+
print(f"An error occurred while copying the versions.txt: {e}")
195+
196+
138197
def _needs_full_repo_generation(generation_config: GenerationConfig) -> bool:
139198
"""
140199
Whether you should need a full repo generation, i.e., generate all

hermetic_build/library_generation/generate_repo.py

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
import shutil
16+
from pathlib import Path
1617
from typing import Optional
1718
import library_generation.utils.utilities as util
1819
from common.model.generation_config import GenerationConfig
@@ -65,6 +66,14 @@ def generate_from_yaml(
6566
repository_path=repository_path, versions_file=repo_config.versions_file
6667
)
6768

69+
# cleanup temp output folder
70+
try:
71+
shutil.rmtree(Path(repo_config.output_folder))
72+
print(f"Directory {repo_config.output_folder} and its contents removed.")
73+
except OSError as e:
74+
print(f"Error: {e} - Failed to remove directory {repo_config.output_folder}.")
75+
raise
76+
6877

6978
def get_target_libraries(
7079
config: GenerationConfig, target_library_names: list[str] = None

hermetic_build/library_generation/tests/cli/entry_point_unit_tests.py

+63
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import os
15+
import shutil
1516
import unittest
17+
from pathlib import Path
1618
from unittest.mock import patch, ANY
1719
from click.testing import CliRunner
1820
from library_generation.cli.entry_point import (
@@ -49,6 +51,20 @@ def test_entry_point_with_invalid_config_raise_file_exception(self):
4951
self.assertEqual(FileNotFoundError, result.exc_info[0])
5052
self.assertRegex(result.exception.args[0], "/non-existent/file does not exist.")
5153

54+
def test_entry_point_with_invalid_generation_input_raise_file_exception(
55+
self,
56+
):
57+
os.chdir(script_dir)
58+
runner = CliRunner()
59+
# noinspection PyTypeChecker
60+
result = runner.invoke(generate, ["--generation-input=/non-existent/folder"])
61+
self.assertEqual(1, result.exit_code)
62+
self.assertEqual(FileNotFoundError, result.exc_info[0])
63+
self.assertRegex(
64+
result.exception.args[0],
65+
"/non-existent/folder/generation_config.yaml does not exist.",
66+
)
67+
5268
def test_validate_generation_config_succeeds(
5369
self,
5470
):
@@ -94,6 +110,7 @@ def test_generate_non_monorepo_without_library_names_full_generation(
94110
# does special handling when a method is annotated with @main.command()
95111
generate_impl(
96112
generation_config_path=config_path,
113+
generation_input=None,
97114
library_names=None,
98115
repository_path=".",
99116
api_definitions_path=".",
@@ -122,6 +139,7 @@ def test_generate_non_monorepo_with_library_names_full_generation(
122139
# does special handling when a method is annotated with @main.command()
123140
generate_impl(
124141
generation_config_path=config_path,
142+
generation_input=None,
125143
library_names="non-existent-library",
126144
repository_path=".",
127145
api_definitions_path=".",
@@ -150,6 +168,7 @@ def test_generate_monorepo_with_common_protos_without_library_names_triggers_ful
150168
# does special handling when a method is annotated with @main.command()
151169
generate_impl(
152170
generation_config_path=config_path,
171+
generation_input=None,
153172
library_names=None,
154173
repository_path=".",
155174
api_definitions_path=".",
@@ -177,6 +196,7 @@ def test_generate_monorepo_with_common_protos_with_library_names_triggers_full_g
177196
# does special handling when a method is annotated with @main.command()
178197
generate_impl(
179198
generation_config_path=config_path,
199+
generation_input=None,
180200
library_names="iam,non-existent-library",
181201
repository_path=".",
182202
api_definitions_path=".",
@@ -206,6 +226,7 @@ def test_generate_monorepo_without_library_names_trigger_full_generation(
206226
# does special handling when a method is annotated with @main.command()
207227
generate_impl(
208228
generation_config_path=config_path,
229+
generation_input=None,
209230
library_names=None,
210231
repository_path=".",
211232
api_definitions_path=".",
@@ -235,6 +256,7 @@ def test_generate_monorepo_with_library_names_trigger_selective_generation(
235256
# does special handling when a method is annotated with @main.command()
236257
generate_impl(
237258
generation_config_path=config_path,
259+
generation_input=None,
238260
library_names="asset",
239261
repository_path=".",
240262
api_definitions_path=".",
@@ -245,3 +267,44 @@ def test_generate_monorepo_with_library_names_trigger_selective_generation(
245267
api_definitions_path=ANY,
246268
target_library_names=["asset"],
247269
)
270+
271+
@patch("library_generation.cli.entry_point.from_yaml")
272+
def test_generate_provide_generation_input(
273+
self,
274+
from_yaml,
275+
):
276+
"""
277+
This test confirms that when no generation_config_path and
278+
only generation_input is provided, it looks inside this path
279+
for generation config and creates versions file when not exists
280+
"""
281+
config_path = f"{test_resource_dir}/generation_config.yaml"
282+
self._create_folder_in_current_dir("test-output")
283+
# we call the implementation method directly since click
284+
# does special handling when a method is annotated with @main.command()
285+
generate_impl(
286+
generation_config_path=None,
287+
generation_input=test_resource_dir,
288+
library_names="asset",
289+
repository_path="./test-output",
290+
api_definitions_path=".",
291+
)
292+
from_yaml.assert_called_with(os.path.abspath(config_path))
293+
self.assertTrue(os.path.exists(f"test-output/versions.txt"))
294+
295+
def tearDown(self):
296+
# clean up after
297+
if os.path.exists("./output"):
298+
shutil.rmtree(Path("./output"))
299+
if os.path.exists("./test-output"):
300+
shutil.rmtree(Path("./test-output"))
301+
302+
def _create_folder_in_current_dir(self, folder_name):
303+
"""Creates a folder in the current directory."""
304+
try:
305+
os.makedirs(
306+
folder_name, exist_ok=True
307+
) # exist_ok prevents errors if folder exists
308+
print(f"Folder '{folder_name}' created successfully.")
309+
except OSError as e:
310+
print(f"Error creating folder '{folder_name}': {e}")

hermetic_build/library_generation/utils/utilities.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,14 @@ def prepare_repo(
159159
json_name = ".repo-metadata.json"
160160
if os.path.exists(f"{absolute_library_path}/{json_name}"):
161161
os.remove(f"{absolute_library_path}/{json_name}")
162-
versions_file = f"{repo_path}/versions.txt"
163-
if not Path(versions_file).exists():
164-
raise FileNotFoundError(f"{versions_file} is not found.")
165-
162+
versions_file = Path(repo_path) / "versions.txt"
163+
if not versions_file.exists():
164+
versions_file.touch()
165+
print(f"Created empty versions file: {versions_file}")
166166
return RepoConfig(
167167
output_folder=output_folder,
168168
libraries=libraries,
169-
versions_file=str(Path(versions_file).resolve()),
169+
versions_file=str(versions_file),
170170
)
171171

172172

0 commit comments

Comments
 (0)