import sys import subprocess import shlex import os import argparse import shutil import logging import coloredlogs class _CombinedFormatter( argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter ): pass logger = logging.getLogger(__name__) coloredlogs.install( fmt="%(asctime)s,%(msecs)03d %(levelname)s - %(message)s", datefmt="%H:%M:%S" ) dest_dir_map = { "dash-core-components": "dcc", "dash-html-components": "html", "dash-table": "dash_table", } def status_print(msg, **kwargs): try: print(msg, **kwargs) except UnicodeEncodeError: print(msg.encode("ascii", "ignore"), **kwargs) def bootstrap_components(components_source, concurrency, install_type): is_windows = sys.platform == "win32" source_glob = ( components_source if components_source != "all" else "{dash-core-components,dash-html-components,dash-table}" ) cmdstr = f"npx lerna exec --concurrency {concurrency} --scope='{source_glob}' -- npm {install_type}" cmd = shlex.split(cmdstr, posix=not is_windows) status_print(cmdstr) with subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows ) as proc: out, err = proc.communicate() status = proc.poll() if err: status_print(("🛑 " if status else "") + err.decode(), file=sys.stderr) if status or not out: status_print( f"🚨 Failed installing npm dependencies for component packages: {source_glob} (status={status}) 🚨", file=sys.stderr, ) sys.exit(1) else: status_print( f"🟢 Finished installing npm dependencies for component packages: {source_glob} 🟢", file=sys.stderr, ) def build_components(components_source, concurrency): is_windows = sys.platform == "win32" source_glob = ( components_source if components_source != "all" else "{dash-core-components,dash-html-components,dash-table}" ) cmdstr = f"npx lerna exec --concurrency {concurrency} --scope='{source_glob}' -- npm run build" cmd = shlex.split(cmdstr, posix=not is_windows) status_print(cmdstr) with subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=is_windows ) as proc: out, err = proc.communicate() status = proc.poll() if err: status_print(("🛑 " if status else "") + err.decode(), file=sys.stderr) if status or not out: status_print( f"🚨 Finished updating component packages: {source_glob} (status={status}) 🚨", file=sys.stderr, ) sys.exit(1) if "{" in source_glob: source_glob = source_glob.split("{")[1].split("}")[0] for package in source_glob.split(","): build_directory = os.path.join( "components", package, package.replace("-", "_").rstrip("/\\") ) dest_dir = dest_dir_map.get(package) or package dest_path = os.path.join("dash", dest_dir) if not os.path.exists(dest_path): try: os.makedirs(dest_path) except OSError: logger.exception("🚨 Having issues manipulating %s", dest_path) sys.exit(1) if not os.path.exists(build_directory): status_print( "🚨 Could not locate build artifacts." + " Check that the npm build process completed" + f" successfully for package: {package} 🚨" ) sys.exit(1) else: status_print(f"🚚 Moving build artifacts from {build_directory} to Dash 🚚") shutil.rmtree(dest_path) shutil.copytree(build_directory, dest_path) with open(os.path.join(dest_path, ".gitkeep"), "w", encoding="utf-8"): pass status_print( f"🟢 Finished moving build artifacts from {build_directory} to Dash 🟢" ) def cli(): parser = argparse.ArgumentParser( prog="dash-update-components", formatter_class=_CombinedFormatter, description="Update the specified subcomponent libraries within Dash" " by copying over build artifacts, dependencies, and dependency metadata.", ) parser.add_argument( "components_source", help="A glob string that matches the Dash component libraries to be updated" " (eg.'dash-table' // 'dash-core-components|dash-html-components' // 'all')." " The default argument is 'all'.", default="all", ) parser.add_argument( "--concurrency", type=int, default=3, help="Maximum concurrent steps, up to 3 (ie all components in parallel)", ) parser.add_argument( "--ci", help="For clean-install use '--ci True'", default="False", ) args = parser.parse_args() if sys.platform == "win32": args.components_source = args.components_source.replace('"', "").replace( "'", "" ) bootstrap_components( args.components_source, args.concurrency, "ci" if args.ci == "True" else "i" ) build_components(args.components_source, args.concurrency) if __name__ == "__main__": cli()