Skip to content

Commit 47f3ee4

Browse files
authored
ENG-7746: include assets in backend.zip (#5814)
1 parent 3d22562 commit 47f3ee4

File tree

3 files changed

+89
-54
lines changed

3 files changed

+89
-54
lines changed

reflex/reflex.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,13 +422,21 @@ def compile(dry: bool, rich: bool):
422422
default=constants.Env.PROD.value,
423423
help="The environment to export the app in.",
424424
)
425+
@click.option(
426+
"--exclude-from-backend",
427+
"backend_excluded_dirs",
428+
multiple=True,
429+
type=click.Path(exists=True, path_type=Path, resolve_path=True),
430+
help="Files or directories to exclude from the backend zip. Can be used multiple times.",
431+
)
425432
def export(
426433
zip: bool,
427434
frontend_only: bool,
428435
backend_only: bool,
429436
zip_dest_dir: str,
430437
upload_db_file: bool,
431438
env: LITERAL_ENV,
439+
backend_excluded_dirs: tuple[Path, ...] = (),
432440
):
433441
"""Export the app to a zip file."""
434442
from reflex.utils import export as export_utils
@@ -455,6 +463,7 @@ def export(
455463
upload_db_file=upload_db_file,
456464
env=constants.Env.DEV if env == constants.Env.DEV else constants.Env.PROD,
457465
loglevel=config.loglevel.subprocess_level(),
466+
backend_excluded_dirs=backend_excluded_dirs,
458467
)
459468

460469

@@ -660,6 +669,13 @@ def makemigrations(message: str | None):
660669
"--config",
661670
help="path to the config file",
662671
)
672+
@click.option(
673+
"--exclude-from-backend",
674+
"backend_excluded_dirs",
675+
multiple=True,
676+
type=click.Path(exists=True, path_type=Path, resolve_path=True),
677+
help="Files or directories to exclude from the backend zip. Can be used multiple times.",
678+
)
663679
def deploy(
664680
app_name: str | None,
665681
app_id: str | None,
@@ -673,6 +689,7 @@ def deploy(
673689
project_name: str | None,
674690
token: str | None,
675691
config_path: str | None,
692+
backend_excluded_dirs: tuple[Path, ...] = (),
676693
):
677694
"""Deploy the app to the Reflex hosting service."""
678695
from reflex_cli.utils import dependency
@@ -721,6 +738,7 @@ def deploy(
721738
zipping=zipping,
722739
loglevel=config.loglevel.subprocess_level(),
723740
upload_db_file=upload_db,
741+
backend_excluded_dirs=backend_excluded_dirs,
724742
)
725743
),
726744
regions=list(region),

reflex/utils/build.py

Lines changed: 67 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -26,64 +26,77 @@ def set_env_json():
2626

2727

2828
def _zip(
29+
*,
2930
component_name: constants.ComponentName,
30-
target: str | Path,
31-
root_dir: str | Path,
32-
exclude_venv_dirs: bool,
33-
upload_db_file: bool = False,
34-
dirs_to_exclude: set[str] | None = None,
35-
files_to_exclude: set[str] | None = None,
36-
top_level_dirs_to_exclude: set[str] | None = None,
31+
target: Path,
32+
root_directory: Path,
33+
exclude_venv_directories: bool,
34+
include_db_file: bool = False,
35+
directory_names_to_exclude: set[str] | None = None,
36+
files_to_exclude: set[Path] | None = None,
3737
globs_to_include: list[str] | None = None,
3838
) -> None:
3939
"""Zip utility function.
4040
4141
Args:
4242
component_name: The name of the component: backend or frontend.
4343
target: The target zip file.
44-
root_dir: The root directory to zip.
45-
exclude_venv_dirs: Whether to exclude venv directories.
46-
upload_db_file: Whether to include local sqlite db files.
47-
dirs_to_exclude: The directories to exclude.
44+
root_directory: The root directory to zip.
45+
exclude_venv_directories: Whether to exclude venv directories.
46+
include_db_file: Whether to include local sqlite db files.
47+
directory_names_to_exclude: The directory names to exclude.
4848
files_to_exclude: The files to exclude.
49-
top_level_dirs_to_exclude: The top level directory names immediately under root_dir to exclude. Do not exclude folders by these names further in the sub-directories.
50-
globs_to_include: Apply these globs from the root_dir and always include them in the zip.
49+
globs_to_include: Apply these globs from the root_directory and always include them in the zip.
5150
5251
"""
5352
target = Path(target)
54-
root_dir = Path(root_dir)
55-
dirs_to_exclude = dirs_to_exclude or set()
53+
root_directory = Path(root_directory).resolve()
54+
directory_names_to_exclude = directory_names_to_exclude or set()
5655
files_to_exclude = files_to_exclude or set()
57-
files_to_zip: list[str] = []
56+
files_to_zip: list[Path] = []
5857
# Traverse the root directory in a top-down manner. In this traversal order,
5958
# we can modify the dirs list in-place to remove directories we don't want to include.
60-
for root, dirs, files in os.walk(root_dir, topdown=True, followlinks=True):
61-
root = Path(root)
59+
for directory_path, subdirectories_names, subfiles_names in os.walk(
60+
root_directory, topdown=True, followlinks=True
61+
):
62+
directory_path = Path(directory_path).resolve()
6263
# Modify the dirs in-place so excluded and hidden directories are skipped in next traversal.
63-
dirs[:] = [
64-
d
65-
for d in dirs
66-
if (basename := Path(d).resolve().name) not in dirs_to_exclude
67-
and not basename.startswith(".")
68-
and (not exclude_venv_dirs or not _looks_like_venv_dir(root / d))
64+
subdirectories_names[:] = [
65+
subdirectory_name
66+
for subdirectory_name in subdirectories_names
67+
if subdirectory_name not in directory_names_to_exclude
68+
and not any(
69+
(directory_path / subdirectory_name).samefile(exclude)
70+
for exclude in files_to_exclude
71+
if exclude.exists()
72+
)
73+
and not subdirectory_name.startswith(".")
74+
and (
75+
not exclude_venv_directories
76+
or not _looks_like_venv_directory(directory_path / subdirectory_name)
77+
)
6978
]
70-
# If we are at the top level with root_dir, exclude the top level dirs.
71-
if top_level_dirs_to_exclude and root == root_dir:
72-
dirs[:] = [d for d in dirs if d not in top_level_dirs_to_exclude]
7379
# Modify the files in-place so the hidden files and db files are excluded.
74-
files[:] = [
75-
f
76-
for f in files
77-
if not f.startswith(".") and (upload_db_file or not f.endswith(".db"))
80+
subfiles_names[:] = [
81+
subfile_name
82+
for subfile_name in subfiles_names
83+
if not subfile_name.startswith(".")
84+
and (include_db_file or not subfile_name.endswith(".db"))
7885
]
7986
files_to_zip += [
80-
str(root / file) for file in files if file not in files_to_exclude
87+
directory_path / subfile_name
88+
for subfile_name in subfiles_names
89+
if not any(
90+
(directory_path / subfile_name).samefile(excluded_file)
91+
for excluded_file in files_to_exclude
92+
if excluded_file.exists()
93+
)
8194
]
8295
if globs_to_include:
8396
for glob in globs_to_include:
8497
files_to_zip += [
85-
str(file)
86-
for file in root_dir.glob(glob)
98+
file
99+
for file in root_directory.glob(glob)
87100
if file.name not in files_to_exclude
88101
]
89102
# Create a progress bar for zipping the component.
@@ -100,56 +113,57 @@ def _zip(
100113
for file in files_to_zip:
101114
console.debug(f"{target}: {file}", progress=progress)
102115
progress.advance(task)
103-
zipf.write(file, Path(file).relative_to(root_dir))
116+
zipf.write(file, Path(file).relative_to(root_directory))
104117

105118

106119
def zip_app(
107120
frontend: bool = True,
108121
backend: bool = True,
109122
zip_dest_dir: str | Path | None = None,
110-
upload_db_file: bool = False,
123+
include_db_file: bool = False,
124+
backend_excluded_dirs: tuple[Path, ...] = (),
111125
):
112126
"""Zip up the app.
113127
114128
Args:
115129
frontend: Whether to zip up the frontend app.
116130
backend: Whether to zip up the backend app.
117131
zip_dest_dir: The directory to export the zip file to.
118-
upload_db_file: Whether to upload the database file.
132+
include_db_file: Whether to include the database file.
133+
backend_excluded_dirs: A tuple of files or directories to exclude from the backend zip. Defaults to ().
119134
"""
120135
zip_dest_dir = zip_dest_dir or Path.cwd()
121136
zip_dest_dir = Path(zip_dest_dir)
122137
files_to_exclude = {
123-
constants.ComponentName.FRONTEND.zip(),
124-
constants.ComponentName.BACKEND.zip(),
138+
Path(constants.ComponentName.FRONTEND.zip()).resolve(),
139+
Path(constants.ComponentName.BACKEND.zip()).resolve(),
125140
}
126141

127142
if frontend:
128143
_zip(
129144
component_name=constants.ComponentName.FRONTEND,
130145
target=zip_dest_dir / constants.ComponentName.FRONTEND.zip(),
131-
root_dir=prerequisites.get_web_dir() / constants.Dirs.STATIC,
146+
root_directory=prerequisites.get_web_dir() / constants.Dirs.STATIC,
132147
files_to_exclude=files_to_exclude,
133-
exclude_venv_dirs=False,
148+
exclude_venv_directories=False,
134149
)
135150

136151
if backend:
137152
_zip(
138153
component_name=constants.ComponentName.BACKEND,
139154
target=zip_dest_dir / constants.ComponentName.BACKEND.zip(),
140-
root_dir=Path.cwd(),
141-
dirs_to_exclude={"__pycache__"},
142-
files_to_exclude=files_to_exclude,
143-
top_level_dirs_to_exclude={"assets"},
144-
exclude_venv_dirs=True,
145-
upload_db_file=upload_db_file,
155+
root_directory=Path.cwd(),
156+
directory_names_to_exclude={"__pycache__"},
157+
files_to_exclude=files_to_exclude | set(backend_excluded_dirs),
158+
exclude_venv_directories=True,
159+
include_db_file=include_db_file,
146160
globs_to_include=[
147161
str(Path(constants.Dirs.WEB) / constants.Dirs.BACKEND / "*")
148162
],
149163
)
150164

151165

152-
def _duplicate_index_html_to_parent_dir(directory: Path):
166+
def _duplicate_index_html_to_parent_directory(directory: Path):
153167
"""Duplicate index.html in the child directories to the given directory.
154168
155169
This makes accessing /route and /route/ work in production.
@@ -169,7 +183,7 @@ def _duplicate_index_html_to_parent_dir(directory: Path):
169183
else:
170184
console.debug(f"Skipping {index_html}, already exists at {target}")
171185
# Recursively call this function for the child directory.
172-
_duplicate_index_html_to_parent_dir(child)
186+
_duplicate_index_html_to_parent_directory(child)
173187

174188

175189
def build():
@@ -200,7 +214,7 @@ def build():
200214
},
201215
)
202216
processes.show_progress("Creating Production Build", process, checkpoints)
203-
_duplicate_index_html_to_parent_dir(wdir / constants.Dirs.STATIC)
217+
_duplicate_index_html_to_parent_directory(wdir / constants.Dirs.STATIC)
204218
path_ops.cp(
205219
wdir / constants.Dirs.STATIC / constants.ReactRouter.SPA_FALLBACK,
206220
wdir / constants.Dirs.STATIC / "404.html",
@@ -247,6 +261,6 @@ def setup_frontend_prod(
247261
build()
248262

249263

250-
def _looks_like_venv_dir(dir_to_check: str | Path) -> bool:
251-
dir_to_check = Path(dir_to_check)
252-
return (dir_to_check / "pyvenv.cfg").exists()
264+
def _looks_like_venv_directory(directory_to_check: str | Path) -> bool:
265+
directory_to_check = Path(directory_to_check)
266+
return (directory_to_check / "pyvenv.cfg").exists()

reflex/utils/export.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def export(
1818
deploy_url: str | None = None,
1919
env: constants.Env = constants.Env.PROD,
2020
loglevel: constants.LogLevel = console._LOG_LEVEL,
21+
backend_excluded_dirs: tuple[Path, ...] = (),
2122
):
2223
"""Export the app to a zip file.
2324
@@ -31,6 +32,7 @@ def export(
3132
deploy_url: The deploy URL to use. Defaults to None.
3233
env: The environment to use. Defaults to constants.Env.PROD.
3334
loglevel: The log level to use. Defaults to console._LOG_LEVEL.
35+
backend_excluded_dirs: A tuple of files or directories to exclude from the backend zip. Defaults to ().
3436
"""
3537
config = get_config()
3638

@@ -70,7 +72,8 @@ def export(
7072
frontend=frontend,
7173
backend=backend,
7274
zip_dest_dir=zip_dest_dir,
73-
upload_db_file=upload_db_file,
75+
include_db_file=upload_db_file,
76+
backend_excluded_dirs=backend_excluded_dirs,
7477
)
7578

7679
# Post a telemetry event.

0 commit comments

Comments
 (0)