build.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. """Building the app and initializing all prerequisites."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import subprocess
  6. import zipfile
  7. from pathlib import Path
  8. from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
  9. from reflex import constants
  10. from reflex.config import get_config
  11. from reflex.utils import console, path_ops, prerequisites, processes
  12. def set_env_json():
  13. """Write the upload url to a REFLEX_JSON."""
  14. path_ops.update_json_file(
  15. constants.Dirs.ENV_JSON,
  16. {endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint},
  17. )
  18. def set_os_env(**kwargs):
  19. """Set os environment variables.
  20. Args:
  21. kwargs: env key word args.
  22. """
  23. for key, value in kwargs.items():
  24. if not value:
  25. continue
  26. os.environ[key.upper()] = value
  27. def generate_sitemap_config(deploy_url: str, export=False):
  28. """Generate the sitemap config file.
  29. Args:
  30. deploy_url: The URL of the deployed app.
  31. export: If the sitemap are generated for an export.
  32. """
  33. # Import here to avoid circular imports.
  34. from reflex.compiler import templates
  35. config = {
  36. "siteUrl": deploy_url,
  37. "generateRobotsTxt": True,
  38. }
  39. if export:
  40. config["outDir"] = constants.Dirs.STATIC
  41. config = json.dumps(config)
  42. with open(constants.Next.SITEMAP_CONFIG_FILE, "w") as f:
  43. f.write(templates.SITEMAP_CONFIG(config=config))
  44. def _zip(
  45. component_name: constants.ComponentName,
  46. target: str,
  47. root_dir: str,
  48. exclude_venv_dirs: bool,
  49. upload_db_file: bool = False,
  50. dirs_to_exclude: set[str] | None = None,
  51. files_to_exclude: set[str] | None = None,
  52. ) -> None:
  53. """Zip utility function.
  54. Args:
  55. component_name: The name of the component: backend or frontend.
  56. target: The target zip file.
  57. root_dir: The root directory to zip.
  58. exclude_venv_dirs: Whether to exclude venv directories.
  59. upload_db_file: Whether to include local sqlite db files.
  60. dirs_to_exclude: The directories to exclude.
  61. files_to_exclude: The files to exclude.
  62. """
  63. dirs_to_exclude = dirs_to_exclude or set()
  64. files_to_exclude = files_to_exclude or set()
  65. files_to_zip: list[str] = []
  66. # Traverse the root directory in a top-down manner. In this traversal order,
  67. # we can modify the dirs list in-place to remove directories we don't want to include.
  68. for root, dirs, files in os.walk(root_dir, topdown=True):
  69. # Modify the dirs in-place so excluded and hidden directories are skipped in next traversal.
  70. dirs[:] = [
  71. d
  72. for d in dirs
  73. if (basename := os.path.basename(os.path.normpath(d)))
  74. not in dirs_to_exclude
  75. and not basename.startswith(".")
  76. and (
  77. not exclude_venv_dirs or not _looks_like_venv_dir(os.path.join(root, d))
  78. )
  79. ]
  80. # Modify the files in-place so the hidden files and db files are excluded.
  81. files[:] = [
  82. f
  83. for f in files
  84. if not f.startswith(".") and (upload_db_file or not f.endswith(".db"))
  85. ]
  86. files_to_zip += [
  87. os.path.join(root, file) for file in files if file not in files_to_exclude
  88. ]
  89. # Create a progress bar for zipping the component.
  90. progress = Progress(
  91. *Progress.get_default_columns()[:-1],
  92. MofNCompleteColumn(),
  93. TimeElapsedColumn(),
  94. )
  95. task = progress.add_task(
  96. f"Zipping {component_name.value}:", total=len(files_to_zip)
  97. )
  98. with progress, zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipf:
  99. for file in files_to_zip:
  100. console.debug(f"{target}: {file}", progress=progress)
  101. progress.advance(task)
  102. zipf.write(file, os.path.relpath(file, root_dir))
  103. def export(
  104. backend: bool = True,
  105. frontend: bool = True,
  106. zip: bool = False,
  107. zip_dest_dir: str = os.getcwd(),
  108. deploy_url: str | None = None,
  109. upload_db_file: bool = False,
  110. ):
  111. """Export the app for deployment.
  112. Args:
  113. backend: Whether to zip up the backend app.
  114. frontend: Whether to zip up the frontend app.
  115. zip: Whether to zip the app.
  116. zip_dest_dir: The destination directory for created zip files (if any)
  117. deploy_url: The URL of the deployed app.
  118. upload_db_file: Whether to include local sqlite db files from the backend zip.
  119. """
  120. # Remove the static folder.
  121. path_ops.rm(constants.Dirs.WEB_STATIC)
  122. # The export command to run.
  123. command = "export"
  124. if frontend:
  125. checkpoints = [
  126. "Linting and checking ",
  127. "Creating an optimized production build",
  128. "Route (pages)",
  129. "prerendered as static HTML",
  130. "Collecting page data",
  131. "Finalizing page optimization",
  132. "Collecting build traces",
  133. ]
  134. # Generate a sitemap if a deploy URL is provided.
  135. if deploy_url is not None:
  136. generate_sitemap_config(deploy_url, export=zip)
  137. command = "export-sitemap"
  138. checkpoints.extend(["Loading next-sitemap", "Generation completed"])
  139. # Start the subprocess with the progress bar.
  140. process = processes.new_process(
  141. [prerequisites.get_package_manager(), "run", command],
  142. cwd=constants.Dirs.WEB,
  143. shell=constants.IS_WINDOWS,
  144. )
  145. processes.show_progress("Creating Production Build", process, checkpoints)
  146. # Zip up the app.
  147. if zip:
  148. files_to_exclude = {
  149. constants.ComponentName.FRONTEND.zip(),
  150. constants.ComponentName.BACKEND.zip(),
  151. }
  152. if frontend:
  153. _zip(
  154. component_name=constants.ComponentName.FRONTEND,
  155. target=os.path.join(
  156. zip_dest_dir, constants.ComponentName.FRONTEND.zip()
  157. ),
  158. root_dir=constants.Dirs.WEB_STATIC,
  159. files_to_exclude=files_to_exclude,
  160. exclude_venv_dirs=False,
  161. )
  162. if backend:
  163. _zip(
  164. component_name=constants.ComponentName.BACKEND,
  165. target=os.path.join(
  166. zip_dest_dir, constants.ComponentName.BACKEND.zip()
  167. ),
  168. root_dir=".",
  169. dirs_to_exclude={"assets", "__pycache__"},
  170. files_to_exclude=files_to_exclude,
  171. exclude_venv_dirs=True,
  172. upload_db_file=upload_db_file,
  173. )
  174. def setup_frontend(
  175. root: Path,
  176. disable_telemetry: bool = True,
  177. ):
  178. """Set up the frontend to run the app.
  179. Args:
  180. root: The root path of the project.
  181. disable_telemetry: Whether to disable the Next telemetry.
  182. """
  183. # Create the assets dir if it doesn't exist.
  184. path_ops.mkdir(constants.Dirs.APP_ASSETS)
  185. # Copy asset files to public folder.
  186. path_ops.cp(
  187. src=str(root / constants.Dirs.APP_ASSETS),
  188. dest=str(root / constants.Dirs.WEB_ASSETS),
  189. )
  190. # Set the environment variables in client (env.json).
  191. set_env_json()
  192. # Disable the Next telemetry.
  193. if disable_telemetry:
  194. processes.new_process(
  195. [
  196. prerequisites.get_package_manager(),
  197. "run",
  198. "next",
  199. "telemetry",
  200. "disable",
  201. ],
  202. cwd=constants.Dirs.WEB,
  203. stdout=subprocess.DEVNULL,
  204. shell=constants.IS_WINDOWS,
  205. )
  206. def setup_frontend_prod(
  207. root: Path,
  208. disable_telemetry: bool = True,
  209. ):
  210. """Set up the frontend for prod mode.
  211. Args:
  212. root: The root path of the project.
  213. disable_telemetry: Whether to disable the Next telemetry.
  214. """
  215. setup_frontend(root, disable_telemetry)
  216. export(deploy_url=get_config().deploy_url)
  217. def _looks_like_venv_dir(dir_to_check: str) -> bool:
  218. return os.path.exists(os.path.join(dir_to_check, "pyvenv.cfg"))