2121
2222import os
2323import re
24+ import shutil
2425import sys
26+ from functools import cache
2527from pathlib import Path
2628from typing import NamedTuple
2729
2830sys .path .insert (0 , str (Path (__file__ ).parent .resolve ()))
29- from in_container_utils import AIRFLOW_CORE_SOURCES_PATH , AIRFLOW_DIST_PATH , click , console , run_command
31+ from in_container_utils import (
32+ AIRFLOW_CORE_SOURCES_PATH ,
33+ AIRFLOW_DIST_PATH ,
34+ AIRFLOW_ROOT_PATH ,
35+ click ,
36+ console ,
37+ run_command ,
38+ )
39+
40+ SOURCE_TARBALL = AIRFLOW_ROOT_PATH / ".build" / "airflow.tar.gz"
41+ EXTRACTED_SOURCE_DIR = AIRFLOW_ROOT_PATH / ".build" / "airflow_source"
42+ CORE_UI_DIST_PREFIX = "ui/dist"
43+ CORE_SOURCE_UI_PREFIX = "airflow-core/src/airflow/ui"
44+ CORE_SOURCE_UI_DIRECTORY = AIRFLOW_CORE_SOURCES_PATH / "airflow" / "ui"
45+ SIMPLE_AUTH_MANAGER_UI_DIST_PREFIX = "api_fastapi/auth/managers/simple/ui/dist"
46+ SIMPLE_AUTH_MANAGER_SOURCE_UI_PREFIX = "airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui"
47+ SIMPLE_AUTH_MANAGER_SOURCE_UI_DIRECTORY = (
48+ AIRFLOW_CORE_SOURCES_PATH / "airflow" / "api_fastapi" / "auth" / "managers" / "simple" / "ui"
49+ )
50+ INTERNAL_SERVER_ERROR = "500 Internal Server Error"
3051
3152
3253def get_provider_name (package_name : str ) -> str :
@@ -208,13 +229,30 @@ def get_providers_constraints_location(
208229 )
209230
210231
232+ @cache
233+ def get_airflow_installation_path () -> Path :
234+ """Get the installation path of Airflow in the container.
235+ Will return somehow like `/usr/python/lib/python3.10/site-packages/airflow`.
236+ """
237+ import importlib .util
238+
239+ spec = importlib .util .find_spec ("airflow" )
240+ if spec is None or spec .origin is None :
241+ console .print ("[red]Airflow not found - cannot mount sources" )
242+ sys .exit (1 )
243+
244+ airflow_path = Path (spec .origin ).parent
245+ return airflow_path
246+
247+
211248class InstallationSpec (NamedTuple ):
212249 airflow_distribution : str | None
213250 airflow_core_distribution : str | None
214251 airflow_constraints_location : str | None
215252 airflow_task_sdk_distribution : str | None
216253 airflow_ctl_distribution : str | None
217254 airflow_ctl_constraints_location : str | None
255+ compile_ui_assets : bool | None
218256 provider_distributions : list [str ]
219257 provider_constraints_location : str | None
220258 pre_release : bool = os .environ .get ("ALLOW_PRE_RELEASES" , "false" ).lower () == "true"
@@ -313,6 +351,7 @@ def find_installation_spec(
313351 else :
314352 airflow_ctl_constraints_location = None
315353 airflow_ctl_distribution = airflow_ctl_spec
354+ compile_ui_assets = False
316355 elif use_airflow_version == "none" or use_airflow_version == "" :
317356 console .print ("\n [bright_blue]Skipping airflow package installation\n " )
318357 airflow_distribution_spec = None
@@ -321,6 +360,7 @@ def find_installation_spec(
321360 airflow_task_sdk_distribution = None
322361 airflow_ctl_distribution = None
323362 airflow_ctl_constraints_location = None
363+ compile_ui_assets = False
324364 elif repo_match := re .match (GITHUB_REPO_BRANCH_PATTERN , use_airflow_version ):
325365 owner , repo , branch = repo_match .groups ()
326366 console .print (f"\n Installing airflow from GitHub: { use_airflow_version } \n " )
@@ -341,6 +381,7 @@ def find_installation_spec(
341381 github_repository = github_repository ,
342382 python_version = python_version ,
343383 )
384+ compile_ui_assets = True
344385 console .print (f"\n Installing airflow task-sdk from GitHub { use_airflow_version } \n " )
345386 airflow_task_sdk_distribution = f"apache-airflow-task-sdk @ { vcs_url } #subdirectory=task-sdk"
346387 airflow_constraints_location = get_airflow_constraints_location (
@@ -365,16 +406,13 @@ def find_installation_spec(
365406 github_repository = github_repository ,
366407 python_version = python_version ,
367408 )
368- console .print (
369- "[yellow]Note that installing airflow from branch has no assets compiled, so you will"
370- "not be able to run UI (we might add asset compilation for this case later if needed)."
371- )
372409 elif use_airflow_version in ["wheel" , "sdist" ] and not use_distributions_from_dist :
373410 console .print (
374411 "[red]USE_AIRFLOW_VERSION cannot be 'wheel' or 'sdist' without --use-distributions-from-dist"
375412 )
376413 sys .exit (1 )
377414 else :
415+ compile_ui_assets = False
378416 console .print (f"\n Installing airflow via apache-airflow=={ use_airflow_version } " )
379417 airflow_distribution_spec = f"apache-airflow{ airflow_extras } =={ use_airflow_version } "
380418 airflow_core_distribution_spec = (
@@ -427,6 +465,7 @@ def find_installation_spec(
427465 airflow_task_sdk_distribution = airflow_task_sdk_distribution ,
428466 airflow_ctl_distribution = airflow_ctl_distribution ,
429467 airflow_ctl_constraints_location = airflow_ctl_constraints_location ,
468+ compile_ui_assets = compile_ui_assets ,
430469 provider_distributions = provider_distributions_list ,
431470 provider_constraints_location = get_providers_constraints_location (
432471 providers_constraints_mode = providers_constraints_mode ,
@@ -443,6 +482,208 @@ def find_installation_spec(
443482 return installation_spec
444483
445484
485+ def download_airflow_source_tarball (installation_spec : InstallationSpec ):
486+ """Download Airflow source tarball from GitHub."""
487+ if not installation_spec .compile_ui_assets :
488+ console .print (
489+ "[bright_blue]Skipping downloading Airflow source tarball since UI assets compilation is disabled."
490+ )
491+ return
492+
493+ if not installation_spec .airflow_distribution :
494+ console .print ("[yellow]No airflow distribution specified, cannot download source tarball." )
495+ return
496+
497+ if SOURCE_TARBALL .exists () and EXTRACTED_SOURCE_DIR .exists ():
498+ console .print (
499+ "[bright_blue]Source tarball and extracted source directory already exist. Skipping download."
500+ )
501+ return
502+
503+ # Extract GitHub repository information from airflow_distribution
504+ # Expected format: "apache-airflow @ git+https://github.com/owner/repo.git@branch"
505+ airflow_dist = installation_spec .airflow_distribution
506+ git_url_match = re .search (r"git\+https://github\.com/([^/]+)/([^/]+)\.git@([^#\s]+)" , airflow_dist )
507+
508+ if not git_url_match :
509+ console .print (f"[yellow]Cannot extract GitHub repository info from: { airflow_dist } " )
510+ return
511+
512+ owner , repo , ref = git_url_match .groups ()
513+ console .print (f"[bright_blue]Downloading source tarball from GitHub: { owner } /{ repo } @{ ref } " )
514+
515+ # Create build directory if it doesn't exist
516+ SOURCE_TARBALL .parent .mkdir (parents = True , exist_ok = True )
517+
518+ # Download tarball from GitHub API if it doesn't exist
519+ if not SOURCE_TARBALL .exists ():
520+ tarball_url = f"https://api.github.com/repos/{ owner } /{ repo } /tarball/{ ref } "
521+ console .print (f"[bright_blue]Downloading from: { tarball_url } " )
522+
523+ try :
524+ result = run_command (
525+ ["curl" , "-L" , tarball_url , "-o" , str (SOURCE_TARBALL )],
526+ github_actions = False ,
527+ shell = False ,
528+ check = True ,
529+ )
530+
531+ if result .returncode != 0 :
532+ console .print (f"[red]Failed to download tarball: { result .stderr } " )
533+ return
534+ except Exception as e :
535+ console .print (f"[red]Error downloading source tarball: { e } " )
536+ return
537+ else :
538+ console .print (f"[bright_blue]Source tarball already exists at: { SOURCE_TARBALL } " )
539+
540+ try :
541+ # Create temporary extraction directory
542+ if EXTRACTED_SOURCE_DIR .exists ():
543+ shutil .rmtree (EXTRACTED_SOURCE_DIR )
544+ # make sure .build exists
545+ EXTRACTED_SOURCE_DIR .parent .mkdir (parents = True , exist_ok = True )
546+
547+ # Extract tarball
548+ console .print (f"[bright_blue]Extracting tarball to: { EXTRACTED_SOURCE_DIR } " )
549+ result = run_command (
550+ ["tar" , "-xzf" , str (SOURCE_TARBALL ), "-C" , str (EXTRACTED_SOURCE_DIR .parent )],
551+ github_actions = False ,
552+ shell = False ,
553+ check = True ,
554+ )
555+
556+ if result .returncode != 0 :
557+ console .print (f"[red]Failed to extract tarball: { result .stderr } " )
558+ return
559+
560+ # Rename extracted directory to a known name
561+ extracted_dirs = list (EXTRACTED_SOURCE_DIR .parent .glob (f"{ owner } -{ repo } -*" ))
562+ if not extracted_dirs :
563+ console .print ("[red]No extracted directory found after tarball extraction." )
564+ return
565+ extracted_dir = extracted_dirs [0 ]
566+ extracted_dir .rename (EXTRACTED_SOURCE_DIR )
567+ console .print ("[bright_blue]Source tarball downloaded and extracted successfully" )
568+
569+ except Exception as e :
570+ console .print (f"[red]Error extracting source tarball: { e } " )
571+ return
572+
573+
574+ def compile_ui_assets (
575+ installation_spec : InstallationSpec ,
576+ source_prefix : str ,
577+ source_ui_directory : Path ,
578+ dist_prefix : str ,
579+ ):
580+ if not installation_spec .compile_ui_assets :
581+ console .print ("[bright_blue]Skipping UI assets compilation" )
582+ return
583+
584+ # Copy UI directories from extracted tarball source to core source directory
585+ extracted_ui_directory = EXTRACTED_SOURCE_DIR / source_prefix
586+ if extracted_ui_directory .exists ():
587+ console .print (
588+ f"[bright_blue]Copying UI source from: { extracted_ui_directory } to: { source_ui_directory } "
589+ )
590+ if source_ui_directory .exists ():
591+ shutil .rmtree (source_ui_directory )
592+ source_ui_directory .parent .mkdir (parents = True , exist_ok = True )
593+ shutil .copytree (extracted_ui_directory , source_ui_directory )
594+ else :
595+ console .print (f"[yellow]Main UI directory not found at: { extracted_ui_directory } " )
596+
597+ if not source_ui_directory .exists ():
598+ console .print (
599+ f"[bright_blue]UI directory '{ source_ui_directory } ' still does not exist. Skipping UI assets compilation."
600+ )
601+ return
602+
603+ # check if UI assets need to be recompiled
604+ dist_directory = get_airflow_installation_path () / dist_prefix
605+ if dist_directory .exists ():
606+ console .print (f"[bright_blue]Already compiled UI assets found in '{ dist_directory } '" )
607+ return
608+ console .print (f"[bright_blue]No compiled UI assets found in '{ dist_directory } '" )
609+
610+ # ensure dependencies for UI assets compilation
611+ need_pnpm = shutil .which ("pnpm" ) is None
612+ if need_pnpm :
613+ console .print ("[bright_blue]Installing pnpm directly from official setup script" )
614+ run_command (
615+ [
616+ "bash" ,
617+ "-c" ,
618+ "curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs" ,
619+ ],
620+ github_actions = False ,
621+ shell = False ,
622+ check = True ,
623+ )
624+ run_command (["npm" , "install" , "-g" , "pnpm" ], github_actions = False , shell = False , check = True )
625+
626+ """
627+ run_command(
628+ [
629+ "bash",
630+ "-c",
631+ 'wget -qO- https://get.pnpm.io/install.sh | ENV="$HOME/.bashrc" SHELL="$(which bash)" bash -',
632+ ],
633+ github_actions=False,
634+ shell=False,
635+ check=True,
636+ )
637+ console.print("[bright_blue]Setting up pnpm PATH")
638+ run_command(
639+ [
640+ "bash",
641+ "-c",
642+ 'export PNPM_HOME="/root/.local/share/pnpm"; case ":$PATH:" in *":$PNPM_HOME:"*) ;; *) export PATH="$PNPM_HOME:$PATH" ;; esac',
643+ ],
644+ github_actions=False,
645+ shell=False,
646+ check=True,
647+ )
648+ """
649+ else :
650+ console .print ("[bright_blue]pnpm already installed" )
651+
652+ # TO avoid ` ELIFECYCLE Command failed.` errors, we need to clear cache and node_modules
653+ run_command (
654+ ["bash" , "-c" , "pnpm cache delete" ],
655+ github_actions = False ,
656+ shell = False ,
657+ check = True ,
658+ cwd = os .fspath (source_ui_directory ),
659+ )
660+ shutil .rmtree (source_ui_directory / "node_modules" , ignore_errors = True )
661+
662+ # install dependencies
663+ run_command (
664+ ["bash" , "-c" , "pnpm install --frozen-lockfile -config.confirmModulesPurge=false" ],
665+ github_actions = False ,
666+ shell = False ,
667+ check = True ,
668+ cwd = os .fspath (source_ui_directory ),
669+ )
670+ # compile UI assets
671+ run_command (
672+ ["bash" , "-c" , "pnpm run build" ],
673+ github_actions = False ,
674+ shell = False ,
675+ check = True ,
676+ cwd = os .fspath (source_ui_directory ),
677+ )
678+ # copy compiled assets to installation directory
679+ dist_source_directory = source_ui_directory / "dist"
680+ console .print (
681+ f"[bright_blue]Copying compiled UI assets from '{ dist_source_directory } ' to '{ dist_directory } '"
682+ )
683+ shutil .copytree (dist_source_directory , dist_directory )
684+ console .print ("[bright_blue]UI assets compiled successfully" )
685+
686+
446687ALLOWED_DISTRIBUTION_FORMAT = ["wheel" , "sdist" , "both" ]
447688ALLOWED_CONSTRAINTS_MODE = ["constraints-source-providers" , "constraints" , "constraints-no-providers" ]
448689ALLOWED_MOUNT_SOURCES = ["remove" , "tests" , "providers-and-tests" , "selected" ]
@@ -660,12 +901,6 @@ def install_airflow_and_providers(
660901 shell = True ,
661902 check = False ,
662903 )
663- import importlib .util
664-
665- spec = importlib .util .find_spec ("airflow" )
666- if spec is None or spec .origin is None :
667- console .print ("[red]Airflow not found - cannot mount sources" )
668- sys .exit (1 )
669904 from packaging .version import Version
670905
671906 from airflow import __version__
@@ -676,7 +911,7 @@ def install_airflow_and_providers(
676911 "[yellow]Patching airflow 2 installation "
677912 "in order to load providers from separate distributions.\n "
678913 )
679- airflow_path = Path ( spec . origin ). parent
914+ airflow_path = get_airflow_installation_path ()
680915 # Make sure old Airflow will include providers including common subfolder allow to extend loading
681916 # providers from the installed separate source packages
682917 console .print ("[yellow]Uninstalling Airflow-3 only providers\n " )
@@ -708,6 +943,15 @@ def install_airflow_and_providers(
708943 airflow_providers_common_init_py .parent .mkdir (exist_ok = True )
709944 airflow_providers_common_init_py .write_text (INIT_CONTENT + "\n " )
710945
946+ # compile ui assets
947+ download_airflow_source_tarball (installation_spec )
948+ compile_ui_assets (installation_spec , CORE_SOURCE_UI_PREFIX , CORE_SOURCE_UI_DIRECTORY , CORE_UI_DIST_PREFIX )
949+ compile_ui_assets (
950+ installation_spec ,
951+ SIMPLE_AUTH_MANAGER_SOURCE_UI_PREFIX ,
952+ SIMPLE_AUTH_MANAGER_SOURCE_UI_DIRECTORY ,
953+ SIMPLE_AUTH_MANAGER_UI_DIST_PREFIX ,
954+ )
711955 console .print ("\n [green]Done!" )
712956
713957
0 commit comments