new options
This commit is contained in:
223
npm_install.py
223
npm_install.py
@@ -191,6 +191,40 @@ def comment_x_served_by_step(path="/etc/angie/conf.d/include/proxy.conf"):
|
|||||||
print(f"✔ Hide X-Served-by header | backup: {backup}")
|
print(f"✔ Hide X-Served-by header | backup: {backup}")
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
def set_file_ownership(files: list[str | Path], owner: str, mode: int | None = None):
|
||||||
|
success = []
|
||||||
|
failed = []
|
||||||
|
|
||||||
|
for file_path in files:
|
||||||
|
path = Path(file_path)
|
||||||
|
|
||||||
|
if not path.exists():
|
||||||
|
failed.append((str(path), "File not found"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
run(["chown", owner, str(path)])
|
||||||
|
|
||||||
|
if mode is not None:
|
||||||
|
os.chmod(path, mode)
|
||||||
|
|
||||||
|
success.append(str(path))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed.append((str(path), str(e)))
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print(f"✔ Set ownership '{owner}' for {len(success)} file(s)")
|
||||||
|
if DEBUG:
|
||||||
|
for f in success:
|
||||||
|
print(f" - {f}")
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
print(f"⚠ Failed to set ownership for {len(failed)} file(s):")
|
||||||
|
for f, err in failed:
|
||||||
|
print(f" - {f}: {err}")
|
||||||
|
|
||||||
|
return len(failed) == 0
|
||||||
|
|
||||||
def download_extract_tar_gz(url: str, dest_dir: Path) -> Path:
|
def download_extract_tar_gz(url: str, dest_dir: Path) -> Path:
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -757,8 +791,94 @@ def adjust_nginx_like_paths_in_tree(root: Path):
|
|||||||
txt = txt.replace("daemon on;", "#daemon on;")
|
txt = txt.replace("daemon on;", "#daemon on;")
|
||||||
cand.write_text(txt, encoding="utf-8")
|
cand.write_text(txt, encoding="utf-8")
|
||||||
|
|
||||||
def install_node_and_yarn(node_pkg: str):
|
def install_node_from_nodesource(version: str):
|
||||||
apt_install([node_pkg])
|
|
||||||
|
version_map = {
|
||||||
|
'latest': '22',
|
||||||
|
'lts': '22',
|
||||||
|
'current': '23'
|
||||||
|
}
|
||||||
|
|
||||||
|
node_version = version_map.get(version.lower(), version)
|
||||||
|
|
||||||
|
match = re.match(r'(\d+)', node_version)
|
||||||
|
if not match:
|
||||||
|
raise ValueError(f"Invalid Node.js version: {version}")
|
||||||
|
|
||||||
|
major_version = match.group(1)
|
||||||
|
|
||||||
|
with step("Removing old Node.js installations"):
|
||||||
|
run(["apt-get", "remove", "-y", "nodejs", "npm", "libnode-dev", "libnode72"], check=False)
|
||||||
|
run(["apt-get", "purge", "-y", "nodejs", "npm", "libnode-dev", "libnode72"], check=False)
|
||||||
|
run(["apt-get", "autoremove", "-y"], check=False)
|
||||||
|
|
||||||
|
for f in ["/etc/apt/sources.list.d/nodesource.list",
|
||||||
|
"/etc/apt/keyrings/nodesource.gpg",
|
||||||
|
"/usr/share/keyrings/nodesource.gpg",
|
||||||
|
"/etc/apt/trusted.gpg.d/nodesource.gpg"]:
|
||||||
|
if Path(f).exists():
|
||||||
|
Path(f).unlink()
|
||||||
|
|
||||||
|
with step(f"Installing Node.js v{major_version}.x from NodeSource repository"):
|
||||||
|
apt_try_install(["ca-certificates", "curl", "gnupg", "apt-transport-https"])
|
||||||
|
|
||||||
|
setup_url = f"https://deb.nodesource.com/setup_{major_version}.x"
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as tf:
|
||||||
|
script_path = tf.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
run(["curl", "-fsSL", setup_url, "-o", script_path])
|
||||||
|
|
||||||
|
# Make it executable
|
||||||
|
os.chmod(script_path, 0o755)
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
subprocess.run(["bash", script_path], check=True)
|
||||||
|
else:
|
||||||
|
run(["bash", script_path])
|
||||||
|
|
||||||
|
run(["apt-get", "update", "-y"])
|
||||||
|
run(["apt-get", "install", "-y", "nodejs"])
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if Path(script_path).exists():
|
||||||
|
os.unlink(script_path)
|
||||||
|
|
||||||
|
if shutil.which("node"):
|
||||||
|
node_ver = run_out(["node", "--version"], check=False).strip()
|
||||||
|
|
||||||
|
installed_major = re.match(r'v?(\d+)', node_ver)
|
||||||
|
if installed_major and installed_major.group(1) != major_version:
|
||||||
|
print(f"⚠ WARNING: Requested Node.js v{major_version}.x but got {node_ver}")
|
||||||
|
print(f" This likely means NodeSource doesn't support your distribution yet.")
|
||||||
|
|
||||||
|
if shutil.which("npm"):
|
||||||
|
npm_ver = run_out(["npm", "--version"], check=False).strip()
|
||||||
|
print(f"✔ Node.js {node_ver} | npm {npm_ver}")
|
||||||
|
else:
|
||||||
|
print(f"✔ Install npm")
|
||||||
|
|
||||||
|
run(["apt-get", "install", "-y", "npm"], check=False)
|
||||||
|
|
||||||
|
if not shutil.which("npm"):
|
||||||
|
run(["corepack", "enable"], check=False)
|
||||||
|
|
||||||
|
if shutil.which("npm"):
|
||||||
|
npm_ver = run_out(["npm", "--version"], check=False).strip()
|
||||||
|
print(f"✔ npm {npm_ver} installed successfully")
|
||||||
|
else:
|
||||||
|
print(f"✖ npm could not be installed - manual intervention required")
|
||||||
|
else:
|
||||||
|
print("✖ Node.js installation failed")
|
||||||
|
raise RuntimeError("Node.js installation failed")
|
||||||
|
|
||||||
|
def install_node_and_yarn(node_pkg: str = None, node_version: str = None):
|
||||||
|
if node_version:
|
||||||
|
install_node_from_nodesource(node_version)
|
||||||
|
else:
|
||||||
|
apt_install([node_pkg or "nodejs"])
|
||||||
|
|
||||||
if shutil.which("yarn") or shutil.which("yarnpkg"):
|
if shutil.which("yarn") or shutil.which("yarnpkg"):
|
||||||
return
|
return
|
||||||
apt_try_install(["yarn"])
|
apt_try_install(["yarn"])
|
||||||
@@ -831,65 +951,131 @@ def _prepare_sass(frontend_dir: Path):
|
|||||||
|
|
||||||
def _build_frontend(src_frontend: Path, dest_frontend: Path):
|
def _build_frontend(src_frontend: Path, dest_frontend: Path):
|
||||||
|
|
||||||
|
|
||||||
def _semver(s: str) -> bool:
|
def _semver(s: str) -> bool:
|
||||||
return bool(re.match(r"^\d+(?:\.\d+){1,3}$", (s or "").strip()))
|
return bool(re.match(r"^\d+(?:\.\d+){1,3}$", (s or "").strip()))
|
||||||
|
|
||||||
def _good_yarn(argv: list[str]) -> bool:
|
def _good_yarn(argv: list[str]) -> bool:
|
||||||
|
try:
|
||||||
v = (run_out(argv + ["--version"], check=False) or "").strip()
|
v = (run_out(argv + ["--version"], check=False) or "").strip()
|
||||||
return _semver(v)
|
return _semver(v)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def _pick_yarn_cmd() -> list[str] | None:
|
def _pick_yarn_cmd() -> list[str] | None:
|
||||||
|
# Try direct yarn/yarnpkg first
|
||||||
for c in (["yarn"], ["yarnpkg"]):
|
for c in (["yarn"], ["yarnpkg"]):
|
||||||
if shutil.which(c[0]) and _good_yarn(c):
|
if shutil.which(c[0]) and _good_yarn(c):
|
||||||
return c
|
return c
|
||||||
if shutil.which("npm") and (run_out(["npm", "--version"], check=False) or "").strip():
|
|
||||||
if _good_yarn(["npm", "exec", "--yes", "yarn@stable"]):
|
# If npm exists, try to use it to run yarn
|
||||||
return ["npm", "exec", "--yes", "yarn@stable"]
|
if shutil.which("npm"):
|
||||||
if shutil.which("npx") and (run_out(["npx", "--version"], check=False) or "").strip():
|
npm_ver = (run_out(["npm", "--version"], check=False) or "").strip()
|
||||||
|
if npm_ver:
|
||||||
|
# Try npm exec yarn@stable
|
||||||
|
if _good_yarn(["npm", "exec", "--yes", "yarn@stable", "--"]):
|
||||||
|
return ["npm", "exec", "--yes", "yarn@stable", "--"]
|
||||||
|
|
||||||
|
# Try npx as fallback
|
||||||
|
if shutil.which("npx"):
|
||||||
|
npx_ver = (run_out(["npx", "--version"], check=False) or "").strip()
|
||||||
|
if npx_ver:
|
||||||
if _good_yarn(["npx", "-y", "yarn@stable"]):
|
if _good_yarn(["npx", "-y", "yarn@stable"]):
|
||||||
return ["npx", "-y", "yarn@stable"]
|
return ["npx", "-y", "yarn@stable"]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _ensure_yarn_installed():
|
def _ensure_yarn_installed():
|
||||||
|
"""Install yarn globally using npm or corepack."""
|
||||||
|
with step("Installing yarn globally"):
|
||||||
if not shutil.which("npm"):
|
if not shutil.which("npm"):
|
||||||
try:
|
try:
|
||||||
apt_try_install(["npm"])
|
apt_try_install(["npm"])
|
||||||
except Exception:
|
except Exception:
|
||||||
run(["apt-get", "update"], check=False)
|
run(["apt-get", "update"], check=False)
|
||||||
run(["apt-get", "install", "-y", "npm"])
|
run(["apt-get", "install", "-y", "npm"])
|
||||||
run(["npm", "install", "-g", "yarn"], check=False)
|
|
||||||
|
# Try corepack first (modern way)
|
||||||
|
if shutil.which("corepack"):
|
||||||
|
try:
|
||||||
|
run(["corepack", "enable"])
|
||||||
|
run(["corepack", "prepare", "yarn@stable", "--activate"])
|
||||||
|
if shutil.which("yarn"):
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to npm install
|
||||||
|
try:
|
||||||
|
run(["npm", "install", "-g", "yarn@latest"])
|
||||||
|
except Exception:
|
||||||
|
# Last resort - try with --force
|
||||||
|
run(["npm", "install", "-g", "--force", "yarn@latest"], check=False)
|
||||||
|
|
||||||
yarn_cmd = _pick_yarn_cmd()
|
yarn_cmd = _pick_yarn_cmd()
|
||||||
if not yarn_cmd:
|
if not yarn_cmd:
|
||||||
_ensure_yarn_installed()
|
_ensure_yarn_installed()
|
||||||
yarn_cmd = _pick_yarn_cmd()
|
yarn_cmd = _pick_yarn_cmd()
|
||||||
|
|
||||||
if not yarn_cmd:
|
if not yarn_cmd:
|
||||||
raise RuntimeError("Unable to detect or install a valid Yarn. Try: apt-get install -y npm && npm i -g yarn.")
|
raise RuntimeError(
|
||||||
|
"Unable to detect or install a valid Yarn.\n"
|
||||||
|
"Try manually: npm install -g yarn@latest"
|
||||||
|
)
|
||||||
|
|
||||||
with step("Installing frontend dependencies (yarn)"):
|
with step("Installing frontend dependencies (yarn)"):
|
||||||
os.environ["NODE_ENV"] = "development"
|
os.environ["NODE_ENV"] = "development"
|
||||||
os.chdir(src_frontend)
|
os.chdir(src_frontend)
|
||||||
_prepare_sass(src_frontend)
|
_prepare_sass(src_frontend)
|
||||||
|
|
||||||
|
# Get and create cache directory
|
||||||
|
try:
|
||||||
cache_dir = (run_out(yarn_cmd + ["cache", "dir"], check=False) or "").strip()
|
cache_dir = (run_out(yarn_cmd + ["cache", "dir"], check=False) or "").strip()
|
||||||
if cache_dir and not Path(cache_dir).exists():
|
if cache_dir and not Path(cache_dir).exists():
|
||||||
Path(cache_dir).mkdir(parents=True, exist_ok=True)
|
Path(cache_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Clean cache
|
||||||
|
try:
|
||||||
run(yarn_cmd + ["cache", "clean"], check=False)
|
run(yarn_cmd + ["cache", "clean"], check=False)
|
||||||
run(yarn_cmd + ["install"])
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
install_cmd = yarn_cmd + ["install"]
|
||||||
|
|
||||||
|
if install_cmd[-1] == "--":
|
||||||
|
install_cmd = install_cmd[:-1]
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
print(f"Running: {' '.join(install_cmd)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
run(install_cmd)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"\n✖ Yarn install failed. Trying with --network-timeout and --ignore-engines...")
|
||||||
|
retry_cmd = install_cmd + ["--network-timeout", "100000", "--ignore-engines"]
|
||||||
|
run(retry_cmd)
|
||||||
|
|
||||||
with step("Building frontend (yarn build)"):
|
with step("Building frontend (yarn build)"):
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["NODE_OPTIONS"] = "--openssl-legacy-provider"
|
env["NODE_OPTIONS"] = "--openssl-legacy-provider"
|
||||||
run(yarn_cmd + ["build"], env=env)
|
|
||||||
|
build_cmd = yarn_cmd + ["build"]
|
||||||
|
if build_cmd[-1] == "--":
|
||||||
|
build_cmd = build_cmd[:-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
run(build_cmd, env=env)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print("\n⚠ Build failed with legacy provider, retrying without...")
|
||||||
|
env.pop("NODE_OPTIONS", None)
|
||||||
|
run(build_cmd, env=env)
|
||||||
|
|
||||||
with step("Copying frontend artifacts"):
|
with step("Copying frontend artifacts"):
|
||||||
shutil.copytree(src_frontend / "dist", dest_frontend, dirs_exist_ok=True)
|
shutil.copytree(src_frontend / "dist", dest_frontend, dirs_exist_ok=True)
|
||||||
if (src_frontend / "app-images").exists():
|
if (src_frontend / "app-images").exists():
|
||||||
shutil.copytree(src_frontend / "app-images", dest_frontend / "images", dirs_exist_ok=True)
|
shutil.copytree(src_frontend / "app-images", dest_frontend / "images", dirs_exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def patch_npm_backend_commands():
|
def patch_npm_backend_commands():
|
||||||
candidates = [
|
candidates = [
|
||||||
Path("/opt/npm/lib/utils.js"),
|
Path("/opt/npm/lib/utils.js"),
|
||||||
@@ -1163,9 +1349,9 @@ def print_summary(info, ipv6_enabled, dark_enabled, update_mode):
|
|||||||
|
|
||||||
# ========== UPDATE-ONLY ==========
|
# ========== UPDATE-ONLY ==========
|
||||||
|
|
||||||
def update_only(node_pkg: str, npm_version_override: str | None, apply_dark: bool, dark_env: dict, ipv6_enabled: bool):
|
def update_only(node_pkg: str, node_version: str | None, npm_version_override: str | None, apply_dark: bool, dark_env: dict, ipv6_enabled: bool):
|
||||||
apt_update_upgrade()
|
apt_update_upgrade()
|
||||||
install_node_and_yarn(node_pkg)
|
install_node_and_yarn(node_pkg=node_pkg, node_version=node_version)
|
||||||
|
|
||||||
version = github_latest_release_tag("NginxProxyManager/nginx-proxy-manager", npm_version_override)
|
version = github_latest_release_tag("NginxProxyManager/nginx-proxy-manager", npm_version_override)
|
||||||
url = f"https://codeload.github.com/NginxProxyManager/nginx-proxy-manager/tar.gz/refs/tags/v{version}"
|
url = f"https://codeload.github.com/NginxProxyManager/nginx-proxy-manager/tar.gz/refs/tags/v{version}"
|
||||||
@@ -1278,6 +1464,7 @@ def main():
|
|||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||||
)
|
)
|
||||||
parser.add_argument("--nodejs-pkg", default="nodejs", help="APT Node.js package name (e.g. nodejs, nodejs-18).")
|
parser.add_argument("--nodejs-pkg", default="nodejs", help="APT Node.js package name (e.g. nodejs, nodejs-18).")
|
||||||
|
parser.add_argument("--node-version", default=None, help="Install Node.js from NodeSource repo (e.g. 'latest', '22', '20', '18'). Overrides --nodejs-pkg.")
|
||||||
parser.add_argument("--npm-version", default=None, help="Force NPM app version (e.g. 2.12.6). Default: latest release.")
|
parser.add_argument("--npm-version", default=None, help="Force NPM app version (e.g. 2.12.6). Default: latest release.")
|
||||||
parser.add_argument("--motd", choices=["yes","no"], default="yes", help="Update MOTD after completion.")
|
parser.add_argument("--motd", choices=["yes","no"], default="yes", help="Update MOTD after completion.")
|
||||||
parser.add_argument("--enable-ipv6", action="store_true",
|
parser.add_argument("--enable-ipv6", action="store_true",
|
||||||
@@ -1310,6 +1497,7 @@ def main():
|
|||||||
fix_logrotate_permissions_and_wrapper()
|
fix_logrotate_permissions_and_wrapper()
|
||||||
version = update_only(
|
version = update_only(
|
||||||
node_pkg=args.nodejs_pkg,
|
node_pkg=args.nodejs_pkg,
|
||||||
|
node_version=args.node_version,
|
||||||
npm_version_override=args.npm_version,
|
npm_version_override=args.npm_version,
|
||||||
apply_dark=args.dark_mode,
|
apply_dark=args.dark_mode,
|
||||||
dark_env=dict(
|
dark_env=dict(
|
||||||
@@ -1333,7 +1521,7 @@ def main():
|
|||||||
|
|
||||||
setup_angie(ipv6_enabled=args.enable_ipv6)
|
setup_angie(ipv6_enabled=args.enable_ipv6)
|
||||||
write_metrics_files()
|
write_metrics_files()
|
||||||
install_node_and_yarn(args.nodejs_pkg)
|
install_node_and_yarn(node_pkg=args.nodejs_pkg, node_version=args.node_version)
|
||||||
ensure_user_and_dirs()
|
ensure_user_and_dirs()
|
||||||
create_sudoers_for_npm()
|
create_sudoers_for_npm()
|
||||||
setup_certbot_venv()
|
setup_certbot_venv()
|
||||||
@@ -1360,6 +1548,11 @@ def main():
|
|||||||
fix_logrotate_permissions_and_wrapper()
|
fix_logrotate_permissions_and_wrapper()
|
||||||
sync_backup_nginx_conf()
|
sync_backup_nginx_conf()
|
||||||
comment_x_served_by_step()
|
comment_x_served_by_step()
|
||||||
|
set_file_ownership(
|
||||||
|
["/etc/nginx/conf.d/include/ip_ranges.conf"],
|
||||||
|
"npm:npm",
|
||||||
|
0o664
|
||||||
|
)
|
||||||
|
|
||||||
with step("Restarting services after installation"):
|
with step("Restarting services after installation"):
|
||||||
run(["systemctl","restart","angie.service"], check=False)
|
run(["systemctl","restart","angie.service"], check=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user