This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-1-test by this push:
     new 0480e437387 [v3-1-test] Auto-compile UI assets on Breeze start-airflow 
command (#57219) (#57253)
0480e437387 is described below

commit 0480e4373872042cf77aa0bab1c678d35abe6c31
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Oct 25 20:20:52 2025 +0200

    [v3-1-test] Auto-compile UI assets on Breeze start-airflow command (#57219) 
(#57253)
    
    * Compile assets when installing airflow from Github Branch in Breeze
    
    * Only install pnpm
    
    * Fix main UI compliation
    
    * Generalize compile_ui_assets for simple_auth_manager ui
    
    * Remove "no assets compiled" warning
    
    * Fix test due to compile_ui_assets not found
    (cherry picked from commit 505d9e423924f0577688ad564755568f2491ed2e)
    
    Co-authored-by: LIU ZHE YOU <[email protected]>
---
 .../in_container/install_airflow_and_providers.py  | 268 ++++++++++++++++++++-
 1 file changed, 256 insertions(+), 12 deletions(-)

diff --git a/scripts/in_container/install_airflow_and_providers.py 
b/scripts/in_container/install_airflow_and_providers.py
index b61b3f39aee..6358e615d00 100755
--- a/scripts/in_container/install_airflow_and_providers.py
+++ b/scripts/in_container/install_airflow_and_providers.py
@@ -21,12 +21,33 @@ from __future__ import annotations
 
 import os
 import re
+import shutil
 import sys
+from functools import cache
 from pathlib import Path
 from typing import NamedTuple
 
 sys.path.insert(0, str(Path(__file__).parent.resolve()))
-from in_container_utils import AIRFLOW_CORE_SOURCES_PATH, AIRFLOW_DIST_PATH, 
click, console, run_command
+from in_container_utils import (
+    AIRFLOW_CORE_SOURCES_PATH,
+    AIRFLOW_DIST_PATH,
+    AIRFLOW_ROOT_PATH,
+    click,
+    console,
+    run_command,
+)
+
+SOURCE_TARBALL = AIRFLOW_ROOT_PATH / ".build" / "airflow.tar.gz"
+EXTRACTED_SOURCE_DIR = AIRFLOW_ROOT_PATH / ".build" / "airflow_source"
+CORE_UI_DIST_PREFIX = "ui/dist"
+CORE_SOURCE_UI_PREFIX = "airflow-core/src/airflow/ui"
+CORE_SOURCE_UI_DIRECTORY = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "ui"
+SIMPLE_AUTH_MANAGER_UI_DIST_PREFIX = "api_fastapi/auth/managers/simple/ui/dist"
+SIMPLE_AUTH_MANAGER_SOURCE_UI_PREFIX = 
"airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui"
+SIMPLE_AUTH_MANAGER_SOURCE_UI_DIRECTORY = (
+    AIRFLOW_CORE_SOURCES_PATH / "airflow" / "api_fastapi" / "auth" / 
"managers" / "simple" / "ui"
+)
+INTERNAL_SERVER_ERROR = "500 Internal Server Error"
 
 
 def get_provider_name(package_name: str) -> str:
@@ -208,6 +229,22 @@ def get_providers_constraints_location(
     )
 
 
+@cache
+def get_airflow_installation_path() -> Path:
+    """Get the installation path of Airflow in the container.
+    Will return somehow like 
`/usr/python/lib/python3.10/site-packages/airflow`.
+    """
+    import importlib.util
+
+    spec = importlib.util.find_spec("airflow")
+    if spec is None or spec.origin is None:
+        console.print("[red]Airflow not found - cannot mount sources")
+        sys.exit(1)
+
+    airflow_path = Path(spec.origin).parent
+    return airflow_path
+
+
 class InstallationSpec(NamedTuple):
     airflow_distribution: str | None
     airflow_core_distribution: str | None
@@ -215,6 +252,7 @@ class InstallationSpec(NamedTuple):
     airflow_task_sdk_distribution: str | None
     airflow_ctl_distribution: str | None
     airflow_ctl_constraints_location: str | None
+    compile_ui_assets: bool | None
     provider_distributions: list[str]
     provider_constraints_location: str | None
     pre_release: bool = os.environ.get("ALLOW_PRE_RELEASES", "false").lower() 
== "true"
@@ -313,6 +351,7 @@ def find_installation_spec(
         else:
             airflow_ctl_constraints_location = None
         airflow_ctl_distribution = airflow_ctl_spec
+        compile_ui_assets = False
     elif use_airflow_version == "none" or use_airflow_version == "":
         console.print("\n[bright_blue]Skipping airflow package installation\n")
         airflow_distribution_spec = None
@@ -321,6 +360,7 @@ def find_installation_spec(
         airflow_task_sdk_distribution = None
         airflow_ctl_distribution = None
         airflow_ctl_constraints_location = None
+        compile_ui_assets = False
     elif repo_match := re.match(GITHUB_REPO_BRANCH_PATTERN, 
use_airflow_version):
         owner, repo, branch = repo_match.groups()
         console.print(f"\nInstalling airflow from GitHub: 
{use_airflow_version}\n")
@@ -341,6 +381,7 @@ def find_installation_spec(
             github_repository=github_repository,
             python_version=python_version,
         )
+        compile_ui_assets = True
         console.print(f"\nInstalling airflow task-sdk from GitHub 
{use_airflow_version}\n")
         airflow_task_sdk_distribution = f"apache-airflow-task-sdk @ 
{vcs_url}#subdirectory=task-sdk"
         airflow_constraints_location = get_airflow_constraints_location(
@@ -365,16 +406,13 @@ def find_installation_spec(
             github_repository=github_repository,
             python_version=python_version,
         )
-        console.print(
-            "[yellow]Note that installing airflow from branch has no assets 
compiled, so you will"
-            "not be able to run UI (we might add asset compilation for this 
case later if needed)."
-        )
     elif use_airflow_version in ["wheel", "sdist"] and not 
use_distributions_from_dist:
         console.print(
             "[red]USE_AIRFLOW_VERSION cannot be 'wheel' or 'sdist' without 
--use-distributions-from-dist"
         )
         sys.exit(1)
     else:
+        compile_ui_assets = False
         console.print(f"\nInstalling airflow via 
apache-airflow=={use_airflow_version}")
         airflow_distribution_spec = 
f"apache-airflow{airflow_extras}=={use_airflow_version}"
         airflow_core_distribution_spec = (
@@ -427,6 +465,7 @@ def find_installation_spec(
         airflow_task_sdk_distribution=airflow_task_sdk_distribution,
         airflow_ctl_distribution=airflow_ctl_distribution,
         airflow_ctl_constraints_location=airflow_ctl_constraints_location,
+        compile_ui_assets=compile_ui_assets,
         provider_distributions=provider_distributions_list,
         provider_constraints_location=get_providers_constraints_location(
             providers_constraints_mode=providers_constraints_mode,
@@ -443,6 +482,208 @@ def find_installation_spec(
     return installation_spec
 
 
+def download_airflow_source_tarball(installation_spec: InstallationSpec):
+    """Download Airflow source tarball from GitHub."""
+    if not installation_spec.compile_ui_assets:
+        console.print(
+            "[bright_blue]Skipping downloading Airflow source tarball since UI 
assets compilation is disabled."
+        )
+        return
+
+    if not installation_spec.airflow_distribution:
+        console.print("[yellow]No airflow distribution specified, cannot 
download source tarball.")
+        return
+
+    if SOURCE_TARBALL.exists() and EXTRACTED_SOURCE_DIR.exists():
+        console.print(
+            "[bright_blue]Source tarball and extracted source directory 
already exist. Skipping download."
+        )
+        return
+
+    # Extract GitHub repository information from airflow_distribution
+    # Expected format: "apache-airflow @ 
git+https://github.com/owner/repo.git@branch";
+    airflow_dist = installation_spec.airflow_distribution
+    git_url_match = 
re.search(r"git\+https://github\.com/([^/]+)/([^/]+)\.git@([^#\s]+)", 
airflow_dist)
+
+    if not git_url_match:
+        console.print(f"[yellow]Cannot extract GitHub repository info from: 
{airflow_dist}")
+        return
+
+    owner, repo, ref = git_url_match.groups()
+    console.print(f"[bright_blue]Downloading source tarball from GitHub: 
{owner}/{repo}@{ref}")
+
+    # Create build directory if it doesn't exist
+    SOURCE_TARBALL.parent.mkdir(parents=True, exist_ok=True)
+
+    # Download tarball from GitHub API if it doesn't exist
+    if not SOURCE_TARBALL.exists():
+        tarball_url = 
f"https://api.github.com/repos/{owner}/{repo}/tarball/{ref}";
+        console.print(f"[bright_blue]Downloading from: {tarball_url}")
+
+        try:
+            result = run_command(
+                ["curl", "-L", tarball_url, "-o", str(SOURCE_TARBALL)],
+                github_actions=False,
+                shell=False,
+                check=True,
+            )
+
+            if result.returncode != 0:
+                console.print(f"[red]Failed to download tarball: 
{result.stderr}")
+                return
+        except Exception as e:
+            console.print(f"[red]Error downloading source tarball: {e}")
+            return
+    else:
+        console.print(f"[bright_blue]Source tarball already exists at: 
{SOURCE_TARBALL}")
+
+    try:
+        # Create temporary extraction directory
+        if EXTRACTED_SOURCE_DIR.exists():
+            shutil.rmtree(EXTRACTED_SOURCE_DIR)
+        # make sure .build exists
+        EXTRACTED_SOURCE_DIR.parent.mkdir(parents=True, exist_ok=True)
+
+        # Extract tarball
+        console.print(f"[bright_blue]Extracting tarball to: 
{EXTRACTED_SOURCE_DIR}")
+        result = run_command(
+            ["tar", "-xzf", str(SOURCE_TARBALL), "-C", 
str(EXTRACTED_SOURCE_DIR.parent)],
+            github_actions=False,
+            shell=False,
+            check=True,
+        )
+
+        if result.returncode != 0:
+            console.print(f"[red]Failed to extract tarball: {result.stderr}")
+            return
+
+        # Rename extracted directory to a known name
+        extracted_dirs = 
list(EXTRACTED_SOURCE_DIR.parent.glob(f"{owner}-{repo}-*"))
+        if not extracted_dirs:
+            console.print("[red]No extracted directory found after tarball 
extraction.")
+            return
+        extracted_dir = extracted_dirs[0]
+        extracted_dir.rename(EXTRACTED_SOURCE_DIR)
+        console.print("[bright_blue]Source tarball downloaded and extracted 
successfully")
+
+    except Exception as e:
+        console.print(f"[red]Error extracting source tarball: {e}")
+        return
+
+
+def compile_ui_assets(
+    installation_spec: InstallationSpec,
+    source_prefix: str,
+    source_ui_directory: Path,
+    dist_prefix: str,
+):
+    if not installation_spec.compile_ui_assets:
+        console.print("[bright_blue]Skipping UI assets compilation")
+        return
+
+    # Copy UI directories from extracted tarball source to core source 
directory
+    extracted_ui_directory = EXTRACTED_SOURCE_DIR / source_prefix
+    if extracted_ui_directory.exists():
+        console.print(
+            f"[bright_blue]Copying UI source from: {extracted_ui_directory} 
to: {source_ui_directory}"
+        )
+        if source_ui_directory.exists():
+            shutil.rmtree(source_ui_directory)
+        source_ui_directory.parent.mkdir(parents=True, exist_ok=True)
+        shutil.copytree(extracted_ui_directory, source_ui_directory)
+    else:
+        console.print(f"[yellow]Main UI directory not found at: 
{extracted_ui_directory}")
+
+    if not source_ui_directory.exists():
+        console.print(
+            f"[bright_blue]UI directory '{source_ui_directory}' still does not 
exist. Skipping UI assets compilation."
+        )
+        return
+
+    # check if UI assets need to be recompiled
+    dist_directory = get_airflow_installation_path() / dist_prefix
+    if dist_directory.exists():
+        console.print(f"[bright_blue]Already compiled UI assets found in 
'{dist_directory}'")
+        return
+    console.print(f"[bright_blue]No compiled UI assets found in 
'{dist_directory}'")
+
+    # ensure dependencies for UI assets compilation
+    need_pnpm = shutil.which("pnpm") is None
+    if need_pnpm:
+        console.print("[bright_blue]Installing pnpm directly from official 
setup script")
+        run_command(
+            [
+                "bash",
+                "-c",
+                "curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && 
apt-get install -y nodejs",
+            ],
+            github_actions=False,
+            shell=False,
+            check=True,
+        )
+        run_command(["npm", "install", "-g", "pnpm"], github_actions=False, 
shell=False, check=True)
+
+        """
+        run_command(
+            [
+                "bash",
+                "-c",
+                'wget -qO- https://get.pnpm.io/install.sh | 
ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -',
+            ],
+            github_actions=False,
+            shell=False,
+            check=True,
+        )
+        console.print("[bright_blue]Setting up pnpm PATH")
+        run_command(
+            [
+                "bash",
+                "-c",
+                'export PNPM_HOME="/root/.local/share/pnpm"; case ":$PATH:" in 
*":$PNPM_HOME:"*) ;; *) export PATH="$PNPM_HOME:$PATH" ;; esac',
+            ],
+            github_actions=False,
+            shell=False,
+            check=True,
+        )
+        """
+    else:
+        console.print("[bright_blue]pnpm already installed")
+
+    # TO avoid ` ELIFECYCLE  Command failed.` errors, we need to clear cache 
and node_modules
+    run_command(
+        ["bash", "-c", "pnpm cache delete"],
+        github_actions=False,
+        shell=False,
+        check=True,
+        cwd=os.fspath(source_ui_directory),
+    )
+    shutil.rmtree(source_ui_directory / "node_modules", ignore_errors=True)
+
+    # install dependencies
+    run_command(
+        ["bash", "-c", "pnpm install --frozen-lockfile 
-config.confirmModulesPurge=false"],
+        github_actions=False,
+        shell=False,
+        check=True,
+        cwd=os.fspath(source_ui_directory),
+    )
+    # compile UI assets
+    run_command(
+        ["bash", "-c", "pnpm run build"],
+        github_actions=False,
+        shell=False,
+        check=True,
+        cwd=os.fspath(source_ui_directory),
+    )
+    # copy compiled assets to installation directory
+    dist_source_directory = source_ui_directory / "dist"
+    console.print(
+        f"[bright_blue]Copying compiled UI assets from 
'{dist_source_directory}' to '{dist_directory}'"
+    )
+    shutil.copytree(dist_source_directory, dist_directory)
+    console.print("[bright_blue]UI assets compiled successfully")
+
+
 ALLOWED_DISTRIBUTION_FORMAT = ["wheel", "sdist", "both"]
 ALLOWED_CONSTRAINTS_MODE = ["constraints-source-providers", "constraints", 
"constraints-no-providers"]
 ALLOWED_MOUNT_SOURCES = ["remove", "tests", "providers-and-tests", "selected"]
@@ -660,12 +901,6 @@ def install_airflow_and_providers(
             shell=True,
             check=False,
         )
-        import importlib.util
-
-        spec = importlib.util.find_spec("airflow")
-        if spec is None or spec.origin is None:
-            console.print("[red]Airflow not found - cannot mount sources")
-            sys.exit(1)
         from packaging.version import Version
 
         from airflow import __version__
@@ -676,7 +911,7 @@ def install_airflow_and_providers(
                 "[yellow]Patching airflow 2 installation "
                 "in order to load providers from separate distributions.\n"
             )
-            airflow_path = Path(spec.origin).parent
+            airflow_path = get_airflow_installation_path()
             # Make sure old Airflow will include providers including common 
subfolder allow to extend loading
             # providers from the installed separate source packages
             console.print("[yellow]Uninstalling Airflow-3 only providers\n")
@@ -708,6 +943,15 @@ def install_airflow_and_providers(
             airflow_providers_common_init_py.parent.mkdir(exist_ok=True)
             airflow_providers_common_init_py.write_text(INIT_CONTENT + "\n")
 
+    # compile ui assets
+    download_airflow_source_tarball(installation_spec)
+    compile_ui_assets(installation_spec, CORE_SOURCE_UI_PREFIX, 
CORE_SOURCE_UI_DIRECTORY, CORE_UI_DIST_PREFIX)
+    compile_ui_assets(
+        installation_spec,
+        SIMPLE_AUTH_MANAGER_SOURCE_UI_PREFIX,
+        SIMPLE_AUTH_MANAGER_SOURCE_UI_DIRECTORY,
+        SIMPLE_AUTH_MANAGER_UI_DIST_PREFIX,
+    )
     console.print("\n[green]Done!")
 
 

Reply via email to