1+ #!/usr/bin/env python3
2+
3+ import argparse
4+ from contextlib import contextmanager
5+ import logging
6+ import os
7+ import tempfile
8+ import urllib .request
9+ import urllib .error
10+ from zipfile import ZipFile
11+
12+
13+ logger = logging .getLogger (__name__ )
14+
15+ ## CONSTANTS
16+ # You can also manually download via `curl -L -O <url>`
17+ # and provide the path to the zip file.
18+
19+ TECHVAR_ZIP_URL = 'https://dpel-assets.aswf.io/usd-alab/alab-techvars.v2.2.0.zip'
20+ BAKED_PROCEDURALS_ZIP_URL = 'https://dpel-assets.aswf.io/usd-alab/alab-procedurals.v2.2.0.zip'
21+ TEXTURE_PACK_ZIP_URL = 'https://aswf-dpel-assets.s3.amazonaws.com/usd-alab/alab-textures.v2.2.0.zip'
22+ CAMERAS_ZIP_URL = 'https://dpel-assets.aswf.io/usd-alab/alab-cameras.v2.2.0.zip'
23+
24+
25+ ## UTILITY FUNCTIONS
26+
27+
28+ @contextmanager
29+ def _get_or_download (url , description , zip_file_path = None ):
30+ """Utility function to either use provided zip file path or download from URL.
31+
32+ Args:
33+ url (str): URL to download from if zip_file_path is None or empty string.
34+ description (str): Description of the file being downloaded, for logging.
35+ zip_file_path (str or None): If provided, path to zip file. If empty
36+ string, download from URL. If None, raise exception.
37+
38+ Returns:
39+ str: Path to zip file, either provided or downloaded.
40+ """
41+ if zip_file_path :
42+ if not os .path .isfile (zip_file_path ):
43+ raise Exception (f'Provided { description } zip file path does not exist: { zip_file_path } ' )
44+ logger .info (f'Using provided { description } zip file: { zip_file_path } ' )
45+ yield zip_file_path
46+ else :
47+ downloaded_tmp_zip = _download (url , description )
48+ try : # delete temp file after use, even in exceptions
49+ yield downloaded_tmp_zip
50+ finally :
51+ os .remove (downloaded_tmp_zip )
52+
53+
54+
55+ def _download (url , description ):
56+ """Utility function to download a file from a URL to a temporary file.
57+
58+ Args:
59+ url (str): URL to download from.
60+ description (str): Description of the file being downloaded, for logging.
61+
62+ Returns:
63+ str: Path to the temporary file containing the downloaded content.
64+ """
65+ logger .info (f'Downloading { description } from { url } ...' )
66+
67+ try :
68+ with urllib .request .urlopen (url ) as response :
69+ if response .getcode () != 200 :
70+ raise Exception (f'Failed to download { description } from { url } . Status code: { response .getcode ()} ' )
71+
72+ tmpfile_path = tempfile .NamedTemporaryFile (delete = False ).name
73+ with open (tmpfile_path , 'wb' ) as tmpfile :
74+ tmpfile .write (response .read ())
75+ except urllib .error .URLError as e :
76+ raise Exception (f'Failed to download { description } from { url } : { e } ' )
77+
78+ return tmpfile_path
79+
80+
81+ def _unzip (zip_file_path , target_folder , zip_file_folder_name ):
82+ """Utility function to unzip zip file into ALab folder.
83+
84+ Args:
85+ zip_file_path (str): Path to the zip file.
86+ target_folder (str): Target folder to unzip into.
87+ zip_file_folder_name (str): Top-level folder name inside the zip file to extract.
88+ """
89+
90+ # assert output folder exists
91+ os .makedirs (target_folder , exist_ok = True )
92+
93+ with ZipFile (zip_file_path , 'r' ) as zip_file :
94+ for member in zip_file .namelist ():
95+ if (zip_file_folder_name + os .sep ) not in member :
96+ continue
97+
98+ relative_path = member .split (zip_file_folder_name + os .sep , 1 )[1 ]
99+ if relative_path :
100+ target_path = os .path .join (target_folder , relative_path )
101+
102+ if member .endswith ('/' ): # a directory
103+ os .makedirs (target_path , exist_ok = True )
104+ else : # a file
105+ os .makedirs (os .path .dirname (target_path ), exist_ok = True )
106+ with open (target_path , 'wb' ) as f :
107+ f .write (zip_file .read (member ))
108+ logger .debug (f'{ member } >> { target_path } ' )
109+
110+
111+ def _parse_args ():
112+ """Parse command line arguments.
113+
114+ Returns:
115+ argparse.Namespace: Parsed arguments.
116+ """
117+ parser = argparse .ArgumentParser (description = 'Build script for assembling ALab packages.' )
118+
119+ default_args = {
120+ 'metavar' : 'ZIP_FILE' ,
121+ 'nargs' : '?' , # allow optional .zip file
122+ 'const' : '' , # indicate download default from URL
123+ 'default' : None , # indicate no action
124+ }
125+
126+ parser .add_argument (
127+ '--output' , '-o' ,
128+ help = 'Output folder for assembled package (defaults to working directory)' ,
129+ default = os .getcwd ()
130+ )
131+
132+ group = parser .add_argument_group ('Install Options' , 'Options to install asset packages into ALab repository. Provide a zip file path, or use without argument to download default online package.' )
133+ group .add_argument (
134+ '--all' ,
135+ action = 'store_true' ,
136+ help = 'Install all asset packages. Individual flags can still be overridden to provide .zip files.'
137+ )
138+ group .add_argument (
139+ '--techvar' ,
140+ help = 'Install Techvar assets' ,
141+ ** default_args ,
142+ )
143+ group .add_argument (
144+ '--baked_procedurals' ,
145+ help = 'Install baked procedurals' ,
146+ ** default_args ,
147+ )
148+ group .add_argument (
149+ '--texture_pack' ,
150+ help = 'Install texture pack' ,
151+ ** default_args ,
152+ )
153+ group .add_argument (
154+ '--cameras' ,
155+ help = 'Install camera assets' ,
156+ ** default_args ,
157+ )
158+
159+ args = parser .parse_args ()
160+ logger .debug (f'Parsed arguments: { args } ' )
161+
162+ if not os .path .isdir (args .output ):
163+ parser .error (f'Output folder does not exist: { args .output } ' )
164+
165+ # if --all is set, set all to default (download)
166+ # but let user override individual options with a .zip file
167+ if args .all :
168+ args .techvar = args .techvar or ''
169+ args .baked_procedurals = args .baked_procedurals or ''
170+ args .texture_pack = args .texture_pack or ''
171+ args .cameras = args .cameras or ''
172+
173+ if not args .all and not any (arg is not None for arg in [args .techvar , args .baked_procedurals , args .texture_pack , args .cameras ]):
174+ parser .error (
175+ 'No action requested. '
176+ 'Add --all to download all packages, or use individual flags: '
177+ '--techvar, --baked_procedurals, --texture_pack, --cameras'
178+ )
179+
180+ return args
181+
182+
183+ ## MAIN FUNCTIONS
184+
185+
186+ def install_techvar (zip_file , output_folder ):
187+ logger .info ('Installing Techvar assets...' )
188+ fragment_folder = os .path .join (output_folder , 'ALab' , 'fragment' )
189+ _unzip (zip_file , fragment_folder , 'fragment' )
190+ logger .info ('Techvar assets installed successfully.' )
191+
192+
193+ def install_baked_procedurals (zip_file , output_folder ):
194+ logger .info ('Installing baked procedural assets...' )
195+ fragment_folder = os .path .join (output_folder , 'ALab' , 'baked_procedurals' )
196+ _unzip (zip_file , fragment_folder , 'baked_procedurals' )
197+ logger .info ('Baked procedurals installed successfully.' )
198+
199+
200+ def install_texture_pack (zip_file , output_folder ):
201+ logger .info ('Installing texture pack...' )
202+ fragment_folder = os .path .join (output_folder , 'ALab' , 'fragment' )
203+ _unzip (zip_file , fragment_folder , 'fragment' )
204+ logger .info ('Texture pack installed successfully.' )
205+
206+
207+ def install_cameras (zip_file , output_folder ):
208+ logger .info ('Installing camera asset package...' )
209+ fragment_folder = os .path .join (output_folder , 'ALab' )
210+ _unzip (zip_file , fragment_folder , 'trailer_cameras' )
211+ logger .info ('Cameras installed successfully.' )
212+
213+
214+ def install_all (output_folder , techvar = None , baked_procedurals = None , texture_pack = None , cameras = None ):
215+ """Main install function to handle all requested packages.
216+ Args:
217+
218+ output_folder (str): Output folder for the assembled package,
219+ assumed to be a local clone of the ALab GitHub repository.
220+ techvar (str or None): If provided, path to Techvar .zip file. If
221+ empty string, download default Techvar assets. If None, skip installation.
222+ baked_procedurals (str or None): If provided, path to Baked Procedurals
223+ .zip file. If empty string, download default baked procedurals assets. If None, skip installation.
224+ texture_pack (str or None): If provided, path to texture pack .zip file. If
225+ empty string, download default Texture Pack. If None, skip installation.
226+ cameras (str or None): If provided, path to cameras .zip file. If
227+ empty string, download default camera assets. If None, skip installation.
228+ """
229+
230+ if techvar is not None :
231+ with _get_or_download (TECHVAR_ZIP_URL , 'Techvar assets' , techvar ) as zip_file :
232+ install_techvar (zip_file , output_folder )
233+
234+ if baked_procedurals is not None :
235+ with _get_or_download (BAKED_PROCEDURALS_ZIP_URL , 'baked procedurals' , baked_procedurals ) as zip_file :
236+ install_baked_procedurals (zip_file , output_folder )
237+
238+ if texture_pack is not None :
239+ with _get_or_download (TEXTURE_PACK_ZIP_URL , 'texture pack' , texture_pack ) as zip_file :
240+ install_texture_pack (zip_file , output_folder )
241+
242+ if cameras is not None :
243+ with _get_or_download (CAMERAS_ZIP_URL , 'cameras' , cameras ) as zip_file :
244+ install_cameras (zip_file , output_folder )
245+
246+
247+ if __name__ == '__main__' :
248+ logging .basicConfig (level = logging .INFO )
249+
250+ args = _parse_args ()
251+ install_all (
252+ output_folder = args .output ,
253+ techvar = args .techvar ,
254+ baked_procedurals = args .baked_procedurals ,
255+ texture_pack = args .texture_pack ,
256+ cameras = args .cameras
257+ )
0 commit comments