99from pathlib import Path
1010
1111import attrs
12+ import fs
1213
1314from bentoml ._internal .bento .bento import ImageInfo
1415from bentoml ._internal .bento .build_config import BentoBuildConfig
1819from bentoml ._internal .configuration import get_quiet_mode
1920from bentoml ._internal .container .frontend .dockerfile import CONTAINER_METADATA
2021from bentoml ._internal .container .frontend .dockerfile import CONTAINER_SUPPORTED_DISTROS
21- from bentoml ._internal .utils .pkg import get_local_bentoml_dependency
2222from bentoml .exceptions import BentoMLConfigException
2323from bentoml .exceptions import BentoMLException
2424
25+ if t .TYPE_CHECKING :
26+ from fs .base import FS
27+
28+ from bentoml ._internal .bento .build_config import BentoEnvSchema
29+
2530if sys .version_info >= (3 , 11 ):
2631 import tomllib
2732else :
@@ -40,6 +45,7 @@ class Image:
4045 python_version : str = DEFAULT_PYTHON_VERSION
4146 commands : t .List [str ] = attrs .field (factory = list )
4247 lock_python_packages : bool = True
48+ pack_git_packages : bool = True
4349 python_requirements : str = ""
4450 post_commands : t .List [str ] = attrs .field (factory = list )
4551 scripts : t .Dict [str , str ] = attrs .field (factory = dict , init = False )
@@ -122,80 +128,135 @@ def run_script(self, script: str) -> t.Self:
122128 self .scripts [script ] = target_script
123129 return self
124130
125- def freeze (self , platform_ : str | None = None ) -> ImageInfo :
131+ def freeze (
132+ self , bento_fs : FS , envs : list [BentoEnvSchema ], platform_ : str | None = None
133+ ) -> ImageInfo :
126134 """Freeze the image to an ImageInfo object for build."""
127- python_requirements = self ._freeze_python_requirements (platform_ )
128- return ImageInfo (
135+ python_requirements = self ._freeze_python_requirements (bento_fs , platform_ )
136+ from importlib import resources
137+
138+ from _bentoml_impl .docker import generate_dockerfile
139+ from bentoml ._internal .utils .filesystem import copy_file_to_fs_folder
140+
141+ # Prepare env/python files
142+ py_folder = fs .path .join ("env" , "python" )
143+ bento_fs .makedirs (py_folder , recreate = True )
144+ reqs_txt = fs .path .join (py_folder , "requirements.txt" )
145+ bento_fs .writetext (reqs_txt , python_requirements )
146+ info = ImageInfo (
129147 base_image = self .base_image ,
130148 python_version = self .python_version ,
131- commands = [ "export UV_COMPILE_BYTECODE=1" , * self .commands ] ,
149+ commands = self .commands ,
132150 python_requirements = python_requirements ,
133151 post_commands = self .post_commands ,
134- scripts = self .scripts ,
135152 )
136-
137- def _freeze_python_requirements (self , platform_ : str | None = None ) -> str :
138- from tempfile import TemporaryDirectory
139-
153+ # Prepare env/docker files
154+ docker_folder = fs .path .join ("env" , "docker" )
155+ bento_fs .makedirs (docker_folder , recreate = True )
156+ dockerfile_path = fs .path .join (docker_folder , "Dockerfile" )
157+ bento_fs .writetext (
158+ dockerfile_path ,
159+ generate_dockerfile (info , bento_fs , enable_buildkit = False , envs = envs ),
160+ )
161+ for script_name , target_path in self .scripts .items ():
162+ copy_file_to_fs_folder (script_name , bento_fs , dst_filename = target_path )
163+
164+ with resources .path (
165+ "bentoml._internal.container.frontend.dockerfile" , "entrypoint.sh"
166+ ) as entrypoint_path :
167+ copy_file_to_fs_folder (str (entrypoint_path ), bento_fs , docker_folder )
168+ return info
169+
170+ def _freeze_python_requirements (
171+ self , bento_fs : FS , platform_ : str | None = None
172+ ) -> str :
140173 from pip_requirements_parser import RequirementsFile
141174
175+ from bentoml ._internal .bento .bentoml_builder import build_bentoml_sdist
176+ from bentoml ._internal .bento .build_config import PythonOptions
142177 from bentoml ._internal .configuration import get_uv_command
143178
144- with TemporaryDirectory (prefix = "bento-reqs-" ) as parent :
145- requirements_in = Path (parent ).joinpath ("requirements.in" )
146- requirements_in .write_text (self .python_requirements )
147- # XXX: RequirementsFile.from_string() does not work due to bugs
148- requirements_file = RequirementsFile .from_file (str (requirements_in ))
149- has_bentoml_req = any (
150- req .name and req .name .lower () == "bentoml" and req .link is not None
151- for req in requirements_file .requirements
179+ py_folder = fs .path .join ("env" , "python" )
180+ bento_fs .makedirs (py_folder , recreate = True )
181+ requirements_in = Path (
182+ bento_fs .getsyspath (fs .path .join (py_folder , "requirements.in" ))
183+ )
184+ requirements_in .write_text (self .python_requirements )
185+ py_req = fs .path .join ("env" , "python" , "requirements.txt" )
186+ requirements_out = Path (bento_fs .getsyspath (py_req ))
187+ # XXX: RequirementsFile.from_string() does not work due to bugs
188+ requirements_file = RequirementsFile .from_file (str (requirements_in ))
189+ has_bentoml_req = any (
190+ req .name and req .name .lower () == "bentoml" and req .link is not None
191+ for req in requirements_file .requirements
192+ )
193+ wheels_folder = fs .path .join ("env" , "python" , "wheels" )
194+ with requirements_in .open ("w" ) as f :
195+ f .write (requirements_file .dumps (preserve_one_empty_line = True ))
196+ if not has_bentoml_req :
197+ sdist_name = build_bentoml_sdist (bento_fs .getsyspath (wheels_folder ))
198+ bento_req = get_bentoml_requirement ()
199+ if bento_req is not None :
200+ logger .info (
201+ "Adding BentoML requirement to the image: %s." , bento_req
202+ )
203+ f .write (f"{ bento_req } \n " )
204+ elif sdist_name is not None :
205+ f .write (f"./wheels/{ sdist_name } \n " )
206+ if not self .lock_python_packages :
207+ requirements_out .parent .mkdir (parents = True , exist_ok = True )
208+ requirements_out .write_text (requirements_in .read_text ())
209+ PythonOptions .fix_dep_urls (
210+ str (requirements_out ),
211+ bento_fs .getsyspath (wheels_folder ),
212+ self .pack_git_packages ,
152213 )
153- with requirements_in .open ("w" ) as f :
154- f .write (requirements_file .dumps (preserve_one_empty_line = True ))
155- if not has_bentoml_req :
156- req = get_bentoml_requirement () or get_local_bentoml_dependency ()
157- f .write (f"{ req } \n " )
158- if not self .lock_python_packages :
159- return requirements_in .read_text ()
160- lock_args = [
161- str (requirements_in ),
162- "--allow-unsafe" ,
163- "--no-header" ,
164- f"--output-file={ requirements_in .with_suffix ('.lock' )} " ,
165- "--emit-index-url" ,
166- "--emit-find-links" ,
167- "--no-annotate" ,
168- ]
169- if get_debug_mode ():
170- lock_args .append ("--verbose" )
171- else :
172- lock_args .append ("--quiet" )
173- logger .info ("Locking PyPI package versions." )
174- if platform_ :
175- lock_args .extend (["--python-platform" , platform_ ])
176- elif platform .system () != "Linux" or platform .machine () != "x86_64" :
177- logger .info (
178- "Locking packages for %s. Pass `--platform` option to specify the platform." ,
179- DEFAULT_LOCK_PLATFORM ,
180- )
181- lock_args .extend (["--python-platform" , DEFAULT_LOCK_PLATFORM ])
182- cmd = [* get_uv_command (), "pip" , "compile" , * lock_args ]
183- try :
184- subprocess .check_call (
185- cmd ,
186- text = True ,
187- stderr = subprocess .DEVNULL if get_quiet_mode () else None ,
188- cwd = parent ,
189- )
190- except subprocess .CalledProcessError as e :
191- raise BentoMLException (f"Failed to lock PyPI packages: { e } " ) from None
192- locked_requirements = ( # uv doesn't preserve global option lines, add them here
193- "\n " .join (option .dumps () for option in requirements_file .options )
214+ return requirements_out .read_text ()
215+ lock_args = [
216+ str (requirements_in ),
217+ "--allow-unsafe" ,
218+ "--no-header" ,
219+ f"--output-file={ requirements_out } " ,
220+ "--emit-index-url" ,
221+ "--emit-find-links" ,
222+ "--no-annotate" ,
223+ ]
224+ if get_debug_mode ():
225+ lock_args .append ("--verbose" )
226+ else :
227+ lock_args .append ("--quiet" )
228+ logger .info ("Locking PyPI package versions." )
229+ if platform_ :
230+ lock_args .extend (["--python-platform" , platform_ ])
231+ elif platform .system () != "Linux" or platform .machine () != "x86_64" :
232+ logger .info (
233+ "Locking packages for %s. Pass `--platform` option to specify the platform." ,
234+ DEFAULT_LOCK_PLATFORM ,
194235 )
195- if locked_requirements :
196- locked_requirements += "\n "
197- locked_requirements += requirements_in .with_suffix (".lock" ).read_text ()
198- return locked_requirements
236+ lock_args .extend (["--python-platform" , DEFAULT_LOCK_PLATFORM ])
237+ cmd = [* get_uv_command (), "pip" , "compile" , * lock_args ]
238+ try :
239+ subprocess .check_call (
240+ cmd ,
241+ text = True ,
242+ stderr = subprocess .DEVNULL if get_quiet_mode () else None ,
243+ cwd = bento_fs .getsyspath (py_folder ),
244+ )
245+ except subprocess .CalledProcessError as e :
246+ raise BentoMLException (f"Failed to lock PyPI packages: { e } " ) from None
247+ locked_requirements = ( # uv doesn't preserve global option lines, add them here
248+ "\n " .join (option .dumps () for option in requirements_file .options )
249+ )
250+ if locked_requirements :
251+ locked_requirements += "\n "
252+ locked_requirements += requirements_out .read_text ()
253+ requirements_out .write_text (locked_requirements )
254+ PythonOptions .fix_dep_urls (
255+ str (requirements_out ),
256+ bento_fs .getsyspath (wheels_folder ),
257+ self .pack_git_packages ,
258+ )
259+ return requirements_out .read_text ()
199260
200261
201262@attrs .define
0 commit comments