build.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. """Building the app and initializing all prerequisites."""
  2. from __future__ import annotations
  3. import os
  4. import zipfile
  5. from pathlib import Path
  6. from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
  7. from reflex import constants
  8. from reflex.config import get_config
  9. from reflex.utils import console, path_ops, prerequisites, processes
  10. from reflex.utils.exec import is_in_app_harness
  11. def set_env_json():
  12. """Write the upload url to a REFLEX_JSON."""
  13. path_ops.update_json_file(
  14. str(prerequisites.get_web_dir() / constants.Dirs.ENV_JSON),
  15. {
  16. **{endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint},
  17. "TEST_MODE": is_in_app_harness(),
  18. },
  19. )
  20. def _zip(
  21. component_name: constants.ComponentName,
  22. target: str | Path,
  23. root_dir: str | Path,
  24. exclude_venv_dirs: bool,
  25. upload_db_file: bool = False,
  26. dirs_to_exclude: set[str] | None = None,
  27. files_to_exclude: set[str] | None = None,
  28. top_level_dirs_to_exclude: set[str] | None = None,
  29. globs_to_include: list[str] | None = None,
  30. ) -> None:
  31. """Zip utility function.
  32. Args:
  33. component_name: The name of the component: backend or frontend.
  34. target: The target zip file.
  35. root_dir: The root directory to zip.
  36. exclude_venv_dirs: Whether to exclude venv directories.
  37. upload_db_file: Whether to include local sqlite db files.
  38. dirs_to_exclude: The directories to exclude.
  39. files_to_exclude: The files to exclude.
  40. 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.
  41. globs_to_include: Apply these globs from the root_dir and always include them in the zip.
  42. """
  43. target = Path(target)
  44. root_dir = Path(root_dir)
  45. dirs_to_exclude = dirs_to_exclude or set()
  46. files_to_exclude = files_to_exclude or set()
  47. files_to_zip: list[str] = []
  48. # Traverse the root directory in a top-down manner. In this traversal order,
  49. # we can modify the dirs list in-place to remove directories we don't want to include.
  50. for root, dirs, files in os.walk(root_dir, topdown=True, followlinks=True):
  51. root = Path(root)
  52. # Modify the dirs in-place so excluded and hidden directories are skipped in next traversal.
  53. dirs[:] = [
  54. d
  55. for d in dirs
  56. if (basename := Path(d).resolve().name) not in dirs_to_exclude
  57. and not basename.startswith(".")
  58. and (not exclude_venv_dirs or not _looks_like_venv_dir(root / d))
  59. ]
  60. # If we are at the top level with root_dir, exclude the top level dirs.
  61. if top_level_dirs_to_exclude and root == root_dir:
  62. dirs[:] = [d for d in dirs if d not in top_level_dirs_to_exclude]
  63. # Modify the files in-place so the hidden files and db files are excluded.
  64. files[:] = [
  65. f
  66. for f in files
  67. if not f.startswith(".") and (upload_db_file or not f.endswith(".db"))
  68. ]
  69. files_to_zip += [
  70. str(root / file) for file in files if file not in files_to_exclude
  71. ]
  72. if globs_to_include:
  73. for glob in globs_to_include:
  74. files_to_zip += [
  75. str(file)
  76. for file in root_dir.glob(glob)
  77. if file.name not in files_to_exclude
  78. ]
  79. # Create a progress bar for zipping the component.
  80. progress = Progress(
  81. *Progress.get_default_columns()[:-1],
  82. MofNCompleteColumn(),
  83. TimeElapsedColumn(),
  84. )
  85. task = progress.add_task(
  86. f"Zipping {component_name.value}:", total=len(files_to_zip)
  87. )
  88. with progress, zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipf:
  89. for file in files_to_zip:
  90. console.debug(f"{target}: {file}", progress=progress)
  91. progress.advance(task)
  92. zipf.write(file, Path(file).relative_to(root_dir))
  93. def zip_app(
  94. frontend: bool = True,
  95. backend: bool = True,
  96. zip_dest_dir: str | Path = Path.cwd(),
  97. upload_db_file: bool = False,
  98. ):
  99. """Zip up the app.
  100. Args:
  101. frontend: Whether to zip up the frontend app.
  102. backend: Whether to zip up the backend app.
  103. zip_dest_dir: The directory to export the zip file to.
  104. upload_db_file: Whether to upload the database file.
  105. """
  106. zip_dest_dir = Path(zip_dest_dir)
  107. files_to_exclude = {
  108. constants.ComponentName.FRONTEND.zip(),
  109. constants.ComponentName.BACKEND.zip(),
  110. }
  111. if frontend:
  112. _zip(
  113. component_name=constants.ComponentName.FRONTEND,
  114. target=zip_dest_dir / constants.ComponentName.FRONTEND.zip(),
  115. root_dir=prerequisites.get_web_dir() / constants.Dirs.STATIC,
  116. files_to_exclude=files_to_exclude,
  117. exclude_venv_dirs=False,
  118. )
  119. if backend:
  120. _zip(
  121. component_name=constants.ComponentName.BACKEND,
  122. target=zip_dest_dir / constants.ComponentName.BACKEND.zip(),
  123. root_dir=Path.cwd(),
  124. dirs_to_exclude={"__pycache__"},
  125. files_to_exclude=files_to_exclude,
  126. top_level_dirs_to_exclude={"assets"},
  127. exclude_venv_dirs=True,
  128. upload_db_file=upload_db_file,
  129. globs_to_include=[
  130. str(Path(constants.Dirs.WEB) / constants.Dirs.BACKEND / "*")
  131. ],
  132. )
  133. def build(
  134. deploy_url: str | None = None,
  135. for_export: bool = False,
  136. ):
  137. """Build the app for deployment.
  138. Args:
  139. deploy_url: The deployment URL.
  140. for_export: Whether the build is for export.
  141. """
  142. wdir = prerequisites.get_web_dir()
  143. # Clean the static directory if it exists.
  144. path_ops.rm(str(wdir / constants.Dirs.BUILD_DIR))
  145. # The export command to run.
  146. command = "export"
  147. checkpoints = [
  148. "building for production",
  149. "building SSR bundle for production",
  150. "SPA Mode: Generated",
  151. ]
  152. # Start the subprocess with the progress bar.
  153. process = processes.new_process(
  154. [*prerequisites.get_js_package_executor(raise_on_none=True)[0], "run", command],
  155. cwd=wdir,
  156. shell=constants.IS_WINDOWS,
  157. env={
  158. **os.environ,
  159. "NO_COLOR": "1",
  160. },
  161. )
  162. processes.show_progress("Creating Production Build", process, checkpoints)
  163. def setup_frontend(
  164. root: Path,
  165. ):
  166. """Set up the frontend to run the app.
  167. Args:
  168. root: The root path of the project.
  169. """
  170. # Create the assets dir if it doesn't exist.
  171. path_ops.mkdir(constants.Dirs.APP_ASSETS)
  172. path_ops.cp(
  173. src=str(root / constants.Dirs.APP_ASSETS),
  174. dest=str(root / prerequisites.get_web_dir() / constants.Dirs.PUBLIC),
  175. ignore=tuple(f"*.{ext}" for ext in constants.Reflex.STYLESHEETS_SUPPORTED),
  176. )
  177. # Set the environment variables in client (env.json).
  178. set_env_json()
  179. # update the last reflex run time.
  180. prerequisites.set_last_reflex_run_time()
  181. def setup_frontend_prod(
  182. root: Path,
  183. ):
  184. """Set up the frontend for prod mode.
  185. Args:
  186. root: The root path of the project.
  187. """
  188. setup_frontend(root)
  189. build(deploy_url=get_config().deploy_url)
  190. def _looks_like_venv_dir(dir_to_check: str | Path) -> bool:
  191. dir_to_check = Path(dir_to_check)
  192. return (dir_to_check / "pyvenv.cfg").exists()