1616
1717import os
1818import logging
19+ import shutil
1920import yaml
2021from importlib .resources import files
2122from seed_env .seeder import Seeder
2223from seed_env .utils import generate_minimal_pyproject_toml
2324from seed_env .git_utils import download_remote_git_file
2425from seed_env .uv_utils import (
26+ set_exact_python_requirement_in_project_toml ,
2527 build_seed_env ,
2628 build_pypi_package ,
2729 merge_project_toml_files ,
30+ replace_dependencies_in_project_toml ,
2831)
2932
3033logging .basicConfig (level = logging .INFO , format = "%(levelname)s: %(message)s" )
@@ -59,13 +62,15 @@ def __init__(
5962 hardware : str ,
6063 build_pypi_package : bool ,
6164 output_dir : str ,
65+ template_pyproject_toml : str = None ,
6266 ):
6367 self .host_name = host_name
6468 self .host_source_type = host_source_type
6569 self .host_github_org_repo = host_github_org_repo
6670 self .host_requirements_file_path = host_requirements_file_path
6771 self .host_commit = host_commit
6872 self .seed_config_input = seed_config
73+ self .template_pyproject_toml = template_pyproject_toml
6974 self .loaded_seed_config = None
7075 self .seed_tag_or_commit = seed_tag_or_commit
7176 self .python_versions = python_version .split ("," )
@@ -143,6 +148,31 @@ def seed_environment(self):
143148 os .makedirs (self .output_dir , exist_ok = True )
144149 self .output_dir = os .path .abspath (self .output_dir )
145150
151+ # Determine the template for pyproject.toml. The explicit CLI argument takes precedence.
152+ template_path = self .template_pyproject_toml
153+ if not template_path and os .path .isfile ("./pyproject.toml" ):
154+ template_path = os .path .abspath ("./pyproject.toml" )
155+ logging .info (
156+ f"Found pyproject.toml in the current directory. Using it as a template: { template_path } "
157+ )
158+
159+ # Pre-flight check: Ensure the output directory root is clean of a pyproject.toml, as we will generate one.
160+ final_pyproject_path = os .path .join (self .output_dir , "pyproject.toml" )
161+ if os .path .isfile (final_pyproject_path ):
162+ # Check for the specific edge case where the output directory is the project root
163+ # and the existing pyproject.toml is the one we are using as a template.
164+ if template_path and os .path .samefile (template_path , final_pyproject_path ):
165+ raise FileExistsError (
166+ f"The output directory ('{ self .output_dir } ') contains a 'pyproject.toml', which was found to be used as a template. "
167+ "Running this would overwrite the original template file. Please use a different --output-dir; or move the"
168+ "existing pyproject.toml to a different location and use the --template-pyproject-toml flag to specify its new location."
169+ )
170+ # General case: the output directory contains a pre-existing pyproject.toml.
171+ raise FileExistsError (
172+ f"A pyproject.toml file already exists in the output directory: { self .output_dir } . "
173+ "Please provide a clean directory or remove the file to avoid accidentaly overwriting it."
174+ )
175+
146176 # Create a directory for storing the downloaded requirements file
147177 self .download_dir = "downloaded_base_and_seed_requirements"
148178 os .makedirs (self .download_dir , exist_ok = True )
@@ -179,30 +209,36 @@ def seed_environment(self):
179209 f"Using { self .seeder .pypi_project_name } at tag/commit { self .seed_tag_or_commit } on { self .seeder .github_org_repo } as seed"
180210 )
181211
182- # Remove pyproject.toml if it exists, as we will generate a new one with merge_project_toml_files
183- pyproject_file = os .path .join (self .output_dir , "pyproject.toml" )
184- if os .path .isfile (pyproject_file ):
185- os .remove (pyproject_file )
186- logging .info (f"Removed existing pyproject.toml file: { pyproject_file } " )
187-
188212 versioned_project_toml_files = []
189213 for python_version in self .python_versions :
190214 # Generate a subdir for each python version
191215 versioned_output_dir = (
192216 self .output_dir + "/python" + python_version .replace ("." , "_" )
193217 )
194- versioned_project_toml_files .append (versioned_output_dir + "/pyproject.toml" )
195218 os .makedirs (versioned_output_dir , exist_ok = True )
219+ versioned_pyproject_path = os .path .join (versioned_output_dir , "pyproject.toml" )
220+ versioned_project_toml_files .append (versioned_pyproject_path )
196221
197222 # 3. Download the seed lock file for the specified Python version
198223 SEED_LOCK_FILE = os .path .abspath (
199224 self .seeder .download_seed_lock_requirement (python_version )
200225 )
201226
202- # 4. Generate a minimal pyproject.toml file for the specified Python version to the output directory
203- generate_minimal_pyproject_toml (
204- self .host_name , python_version , versioned_output_dir
205- )
227+ # 4. Generate a pyproject.toml file for the specified Python version.
228+ if template_path :
229+ logging .info (f"Using template { template_path } for Python { python_version } " )
230+ shutil .copy (template_path , versioned_pyproject_path )
231+ # Clear any existing dependencies from the template to start fresh.
232+ replace_dependencies_in_project_toml ([], versioned_pyproject_path )
233+ # Update the python version in the copied template to be specific for this build pass.
234+ set_exact_python_requirement_in_project_toml (
235+ python_version , versioned_pyproject_path
236+ )
237+ else :
238+ logging .info (f"Generating minimal pyproject.toml for Python { python_version } " )
239+ generate_minimal_pyproject_toml (
240+ self .host_name , python_version , versioned_output_dir
241+ )
206242
207243 # Construct the host lock file name
208244 HOST_LOCK_FILE_NAME = f"{ self .host_name .replace ('-' , '_' )} _requirements_lock_{ python_version .replace ('.' , '_' )} .txt"
@@ -220,6 +256,12 @@ def seed_environment(self):
220256 merge_project_toml_files (versioned_project_toml_files , self .output_dir )
221257
222258 # 6. Build pypi package
259+ # TODO(kanglant): Assume where the seed-env cli is called is the project root
260+ # and move the generated pyproject.toml to the project root? If there is an old
261+ # pyproject.toml here, this behavior will overwrite it. I think this is risky.
262+ # Another option is to copy the source files, specified in the new pyproject.toml,
263+ # from the project root to the output_dir folder. Then perform a fully isolated
264+ # build at the output_dir.
223265 if self .build_pypi_package :
224266 # Use the new pyproject.toml file at the output dir to build the package.
225267 build_pypi_package (self .output_dir )
0 commit comments