| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| # |
| |
| |
| """ |
| PyInstaller build script (Python version) |
| """ |
| |
| import os |
| import shutil |
| import subprocess |
| import sys |
| from pathlib import Path |
| |
| |
| def get_venv_base_dir(): |
| """ |
| Get the base directory for virtual environments outside the project. |
| |
| Returns: |
| Path: Base directory path |
| - Linux/macOS: ~/.cache/iotdb-ainode-build/ |
| - Windows: %LOCALAPPDATA%\\iotdb-ainode-build\\ |
| """ |
| if sys.platform == "win32": |
| localappdata = os.environ.get("LOCALAPPDATA") or os.environ.get( |
| "APPDATA", os.path.expanduser("~") |
| ) |
| base_dir = Path(localappdata) / "iotdb-ainode-build" |
| else: |
| base_dir = Path.home() / ".cache" / "iotdb-ainode-build" |
| |
| return base_dir |
| |
| |
| def setup_venv(): |
| """ |
| Create virtual environment outside the project directory. |
| |
| The virtual environment is created in a platform-specific location: |
| - Linux/macOS: ~/.cache/iotdb-ainode-build/<project-name>/ |
| - Windows: %LOCALAPPDATA%\\iotdb-ainode-build\\<project-name>\\ |
| |
| The same venv is reused across multiple builds of the same project. |
| |
| Returns: |
| Path: Path to the virtual environment directory |
| """ |
| script_dir = Path(__file__).parent |
| venv_base_dir = get_venv_base_dir() |
| venv_dir = venv_base_dir / script_dir.name |
| |
| if venv_dir.exists(): |
| print(f"Virtual environment already exists at: {venv_dir}") |
| return venv_dir |
| |
| venv_base_dir.mkdir(parents=True, exist_ok=True) |
| print(f"Creating virtual environment at: {venv_dir}") |
| subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True) |
| print("Virtual environment created successfully") |
| return venv_dir |
| |
| |
| def get_venv_python(venv_dir): |
| """Get Python executable path in virtual environment""" |
| if sys.platform == "win32": |
| return venv_dir / "Scripts" / "python.exe" |
| else: |
| return venv_dir / "bin" / "python" |
| |
| |
| def update_pip(venv_python): |
| """Update pip in the virtual environment to the latest version.""" |
| print("Updating pip...") |
| subprocess.run( |
| [str(venv_python), "-m", "pip", "install", "--upgrade", "pip"], check=True |
| ) |
| print("pip updated successfully") |
| |
| |
| def install_poetry(venv_python): |
| """Install poetry 2.2.1 in the virtual environment.""" |
| print("Installing poetry 2.2.1...") |
| subprocess.run( |
| [ |
| str(venv_python), |
| "-m", |
| "pip", |
| "install", |
| "poetry==2.2.1", |
| ], |
| check=True, |
| ) |
| # Get installed version |
| version_result = subprocess.run( |
| [str(venv_python), "-m", "poetry", "--version"], |
| capture_output=True, |
| text=True, |
| check=True, |
| ) |
| print(f"Poetry installed: {version_result.stdout.strip()}") |
| |
| |
| def get_venv_env(venv_dir): |
| """ |
| Get environment variables configured for the virtual environment. |
| |
| Sets VIRTUAL_ENV and prepends the venv's bin/Scripts directory to PATH |
| so that tools installed in the venv take precedence. |
| Also sets POETRY_VIRTUALENVS_PATH to force poetry to use our venv. |
| |
| Returns: |
| dict: Environment variables dictionary |
| """ |
| env = os.environ.copy() |
| env["VIRTUAL_ENV"] = str(venv_dir.absolute()) |
| |
| venv_bin = str(venv_dir / ("Scripts" if sys.platform == "win32" else "bin")) |
| env["PATH"] = f"{venv_bin}{os.pathsep}{env.get('PATH', '')}" |
| |
| # Force poetry to use our virtual environment by setting POETRY_VIRTUALENVS_PATH |
| # This tells poetry where to look for/create virtual environments |
| env["POETRY_VIRTUALENVS_PATH"] = str(venv_dir.parent.absolute()) |
| |
| return env |
| |
| |
| def get_poetry_executable(venv_dir): |
| """Get poetry executable path in the virtual environment.""" |
| if sys.platform == "win32": |
| return venv_dir / "Scripts" / "poetry.exe" |
| else: |
| return venv_dir / "bin" / "poetry" |
| |
| |
| def install_dependencies(venv_python, venv_dir, script_dir): |
| """ |
| Install project dependencies using poetry. |
| |
| Configures poetry to use the external virtual environment and installs |
| all dependencies from pyproject.toml. |
| """ |
| print("Installing dependencies with poetry...") |
| venv_env = get_venv_env(venv_dir) |
| poetry_exe = get_poetry_executable(venv_dir) |
| |
| # Configure poetry settings |
| print("Configuring poetry settings...") |
| try: |
| # Set poetry to not create venvs in project directory |
| subprocess.run( |
| [str(poetry_exe), "config", "virtualenvs.in-project", "false"], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=True, |
| capture_output=True, |
| text=True, |
| ) |
| # Set poetry virtualenvs path to our venv directory's parent |
| # This forces poetry to look for/create venvs in the same location as our venv |
| subprocess.run( |
| [ |
| str(poetry_exe), |
| "config", |
| "virtualenvs.path", |
| str(venv_dir.parent.absolute()), |
| ], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=True, |
| capture_output=True, |
| text=True, |
| ) |
| # Ensure poetry can use virtual environments |
| subprocess.run( |
| [str(poetry_exe), "config", "virtualenvs.create", "true"], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=True, |
| capture_output=True, |
| text=True, |
| ) |
| except Exception as e: |
| print(f"Warning: Failed to configure poetry settings: {e}") |
| # Continue anyway, as these may not be critical |
| |
| # Remove any existing poetry virtual environments for this project |
| # This ensures poetry will use our specified virtual environment |
| print("Removing any existing poetry virtual environments...") |
| remove_result = subprocess.run( |
| [str(poetry_exe), "env", "remove", "--all"], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=False, # Don't fail if no venv exists |
| capture_output=True, |
| text=True, |
| ) |
| if remove_result.stdout: |
| print(remove_result.stdout.strip()) |
| if remove_result.stderr: |
| stderr = remove_result.stderr.strip() |
| # Ignore "No virtualenv has been activated" error |
| if "no virtualenv" not in stderr.lower(): |
| print(remove_result.stderr.strip()) |
| |
| # Verify the virtual environment Python is valid before configuring poetry |
| print(f"Verifying virtual environment Python at: {venv_python}") |
| if not venv_python.exists(): |
| print(f"ERROR: Virtual environment Python not found at: {venv_python}") |
| sys.exit(1) |
| |
| python_version_result = subprocess.run( |
| [str(venv_python), "--version"], |
| capture_output=True, |
| text=True, |
| check=False, |
| ) |
| if python_version_result.returncode != 0: |
| print(f"ERROR: Virtual environment Python is not executable: {venv_python}") |
| sys.exit(1) |
| print(f" Python version: {python_version_result.stdout.strip()}") |
| |
| # Instead of using poetry env use (which creates new venvs), we'll use a different approach: |
| # 1. Create a symlink from poetry's expected venv location to our venv |
| # 2. Or, directly use poetry install with VIRTUAL_ENV set (poetry should detect it) |
| # |
| # The issue is that poetry env use creates venvs with hash-based names in its cache. |
| # We need to work around this by either: |
| # - Creating a symlink from poetry's expected location to our venv |
| # - Or bypassing poetry env use entirely and using poetry install directly |
| |
| # Strategy: Create a symlink from poetry's expected venv location to our venv |
| # Poetry creates venvs with names like: <project-name>-<hash>-py<python-version> |
| # We need to find out what poetry would name our venv, then create a symlink |
| |
| print(f"Configuring poetry to use virtual environment at: {venv_dir}") |
| |
| # Get poetry's expected venv name by checking what it would create |
| # First, let's try poetry env use, but catch if it tries to create a new venv |
| result = subprocess.run( |
| [str(poetry_exe), "env", "use", str(venv_python)], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=False, |
| capture_output=True, |
| text=True, |
| ) |
| |
| output_text = (result.stdout or "") + (result.stderr or "") |
| |
| # If poetry is creating a new venv, we need to stop it and use a different approach |
| if ( |
| "Creating virtualenv" in output_text |
| or "Creating virtual environment" in output_text |
| or "Using virtualenv:" in output_text |
| ): |
| print("Poetry is attempting to create/use a new virtual environment.") |
| print( |
| "Stopping this and using alternative approach: creating symlink to our venv..." |
| ) |
| |
| # Extract the venv path poetry is trying to create/use |
| # Look for patterns like "Using virtualenv: /path/to/venv" or "Creating virtualenv name in /path" |
| import re |
| |
| poetry_venv_path = None |
| |
| # Try to extract from "Using virtualenv: /path/to/venv" |
| using_match = re.search(r"Using virtualenv:\s*([^\s\n]+)", output_text) |
| if using_match: |
| poetry_venv_path = Path(using_match.group(1)) |
| |
| # If not found, try to extract from "Creating virtualenv name in /path" |
| if not poetry_venv_path: |
| creating_match = re.search( |
| r"Creating virtualenv[^\n]*in\s+([^\s\n]+)", output_text |
| ) |
| if creating_match: |
| venv_dir_path = Path(creating_match.group(1)) |
| # Extract venv name from the output |
| name_match = re.search(r"Creating virtualenv\s+([^\s]+)", output_text) |
| if name_match: |
| venv_name = name_match.group(1) |
| poetry_venv_path = venv_dir_path / venv_name |
| |
| # If still not found, try to find any path in pypoetry/virtualenvs |
| if not poetry_venv_path: |
| pypoetry_match = re.search( |
| r"([^\s]+pypoetry[^\s]*virtualenvs[^\s]+)", output_text |
| ) |
| if pypoetry_match: |
| poetry_venv_path = Path(pypoetry_match.group(1)) |
| |
| if poetry_venv_path: |
| print(f"Poetry wants to create/use venv at: {poetry_venv_path}") |
| |
| # Remove the venv poetry just created (if it exists) |
| if poetry_venv_path.exists() and poetry_venv_path.is_dir(): |
| print(f"Removing poetry's newly created venv: {poetry_venv_path}") |
| shutil.rmtree(poetry_venv_path, ignore_errors=True) |
| |
| # Create a symlink from poetry's expected location to our venv |
| print(f"Creating symlink from {poetry_venv_path} to {venv_dir}") |
| try: |
| if poetry_venv_path.exists() or poetry_venv_path.is_symlink(): |
| if poetry_venv_path.is_symlink(): |
| poetry_venv_path.unlink() |
| elif poetry_venv_path.is_dir(): |
| shutil.rmtree(poetry_venv_path, ignore_errors=True) |
| poetry_venv_path.parent.mkdir(parents=True, exist_ok=True) |
| poetry_venv_path.symlink_to(venv_dir) |
| print(f"Symlink created successfully") |
| except Exception as e: |
| print(f"WARNING: Failed to create symlink: {e}") |
| print("Will try to use poetry install directly with VIRTUAL_ENV set") |
| else: |
| print("Could not determine poetry's venv path from output") |
| print(f"Output was: {output_text}") |
| else: |
| if result.stdout: |
| print(result.stdout.strip()) |
| if result.stderr: |
| stderr = result.stderr.strip() |
| if stderr: |
| print(f"Poetry output: {stderr}") |
| |
| # Verify poetry is using the correct virtual environment BEFORE running lock/install |
| # This is critical - if poetry uses the wrong venv, dependencies won't be installed correctly |
| print("Verifying poetry virtual environment...") |
| |
| # Wait a moment for symlink to be recognized (if we created one) |
| import time |
| |
| time.sleep(0.5) |
| |
| verify_result = subprocess.run( |
| [str(poetry_exe), "env", "info", "--path"], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=False, # Don't fail if poetry hasn't activated a venv yet |
| capture_output=True, |
| text=True, |
| ) |
| |
| expected_venv_path_resolved = str(Path(venv_dir.absolute()).resolve()) |
| |
| # If poetry env info fails, it might mean poetry hasn't activated the venv yet |
| if verify_result.returncode != 0: |
| print( |
| "Warning: poetry env info failed, poetry may not have activated the virtual environment yet" |
| ) |
| print( |
| "This may be okay if we created a symlink - poetry should use it when running commands" |
| ) |
| poetry_venv_path_resolved = None |
| else: |
| poetry_venv_path = verify_result.stdout.strip() |
| |
| # Normalize paths for comparison (resolve symlinks, etc.) |
| poetry_venv_path_resolved = str(Path(poetry_venv_path).resolve()) |
| |
| # Only verify path if we successfully got poetry's venv path |
| if poetry_venv_path_resolved is not None: |
| if poetry_venv_path_resolved != expected_venv_path_resolved: |
| print( |
| f"ERROR: Poetry is using {poetry_venv_path}, but expected {expected_venv_path_resolved}" |
| ) |
| print( |
| "Poetry must use the virtual environment we created for the build to work correctly." |
| ) |
| print("The symlink approach may not have worked. Please check the symlink.") |
| sys.exit(1) |
| else: |
| print(f"Poetry is correctly using virtual environment: {poetry_venv_path}") |
| else: |
| print("Warning: Could not verify poetry virtual environment path") |
| print( |
| "Continuing anyway - poetry should use the venv via symlink or VIRTUAL_ENV" |
| ) |
| |
| # Update lock file and install dependencies |
| # Re-verify environment before each command to ensure poetry doesn't switch venvs |
| def verify_poetry_env(): |
| verify_result = subprocess.run( |
| [str(poetry_exe), "env", "info", "--path"], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=False, # Don't fail if poetry env info is not available |
| capture_output=True, |
| text=True, |
| ) |
| if verify_result.returncode == 0: |
| current_path = str(Path(verify_result.stdout.strip()).resolve()) |
| expected_path = str(Path(venv_dir.absolute()).resolve()) |
| if current_path != expected_path: |
| print( |
| f"ERROR: Poetry switched to different virtual environment: {current_path}" |
| ) |
| print(f"Expected: {expected_path}") |
| sys.exit(1) |
| # If poetry env info fails, we can't verify, but continue anyway |
| # Poetry should still use the Python we specified via env use |
| return True |
| |
| print("Running poetry lock...") |
| verify_poetry_env() # Verify before lock |
| result = subprocess.run( |
| [str(poetry_exe), "lock"], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=True, |
| capture_output=True, |
| text=True, |
| ) |
| if result.stdout: |
| print(result.stdout) |
| if result.stderr: |
| print(result.stderr) |
| verify_poetry_env() # Verify after lock |
| |
| print("Running poetry install...") |
| subprocess.run( |
| [str(poetry_exe), "lock"], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=True, |
| capture_output=True, |
| text=True, |
| ) |
| verify_poetry_env() # Verify before install |
| result = subprocess.run( |
| [str(poetry_exe), "install"], |
| cwd=str(script_dir), |
| env=venv_env, |
| check=True, |
| capture_output=True, |
| text=True, |
| ) |
| if result.stdout: |
| print(result.stdout) |
| if result.stderr: |
| print(result.stderr) |
| verify_poetry_env() # Verify after install |
| |
| # Verify installation by checking if key packages are installed |
| # This is critical - if packages aren't installed, PyInstaller won't find them |
| print("Verifying package installation...") |
| test_packages = ["torch", "transformers", "tokenizers"] |
| missing_packages = [] |
| for package in test_packages: |
| test_result = subprocess.run( |
| [str(venv_python), "-c", f"import {package}; print({package}.__version__)"], |
| capture_output=True, |
| text=True, |
| check=False, |
| ) |
| if test_result.returncode == 0: |
| version = test_result.stdout.strip() |
| print(f"{package} {version} installed") |
| else: |
| error_msg = ( |
| test_result.stderr.strip() if test_result.stderr else "Unknown error" |
| ) |
| print(f"{package} NOT found in virtual environment: {error_msg}") |
| missing_packages.append(package) |
| |
| if missing_packages: |
| print( |
| f"\nERROR: Required packages are missing from virtual environment: {', '.join(missing_packages)}" |
| ) |
| print("This indicates that poetry did not install dependencies correctly.") |
| print("Please check the poetry install output above for errors.") |
| sys.exit(1) |
| |
| print("Dependencies installed successfully") |
| |
| |
| def check_pyinstaller(venv_python): |
| """ |
| Check if PyInstaller is installed. |
| |
| PyInstaller should be installed via poetry install from pyproject.toml. |
| If it's missing, it means poetry install failed or didn't complete. |
| """ |
| try: |
| result = subprocess.run( |
| [ |
| str(venv_python), |
| "-c", |
| "import PyInstaller; print(PyInstaller.__version__)", |
| ], |
| capture_output=True, |
| text=True, |
| check=True, |
| ) |
| version = result.stdout.strip() |
| print(f"PyInstaller version: {version}") |
| return True |
| except (subprocess.CalledProcessError, FileNotFoundError): |
| print("ERROR: PyInstaller is not installed in the virtual environment") |
| print("PyInstaller should be installed via poetry install from pyproject.toml") |
| print( |
| "This indicates that poetry install may have failed or didn't complete correctly." |
| ) |
| return False |
| |
| |
| def build(): |
| """ |
| Execute the complete build process. |
| |
| Steps: |
| 1. Setup virtual environment (outside project directory) |
| 2. Update pip and install 2.2.1 poetry |
| 3. Install project dependencies (including PyInstaller from pyproject.toml) |
| 4. Build executable using PyInstaller |
| """ |
| script_dir = Path(__file__).parent |
| |
| venv_dir = setup_venv() |
| venv_python = get_venv_python(venv_dir) |
| |
| update_pip(venv_python) |
| install_poetry(venv_python) |
| install_dependencies(venv_python, venv_dir, script_dir) |
| |
| if not check_pyinstaller(venv_python): |
| sys.exit(1) |
| |
| print("=" * 50) |
| print("IoTDB AINode PyInstaller Build Script") |
| print("=" * 50) |
| print() |
| |
| print("Starting build...") |
| print() |
| |
| spec_file = script_dir / "ainode.spec" |
| if not spec_file.exists(): |
| print(f"Error: Spec file not found: {spec_file}") |
| sys.exit(1) |
| |
| # Set up environment for PyInstaller |
| # When using venv_python, PyInstaller should automatically detect the virtual environment |
| # and use its site-packages. We should NOT manually add site-packages to pathex. |
| pyinstaller_env = get_venv_env(venv_dir) |
| |
| # Verify we're using the correct Python |
| python_prefix_result = subprocess.run( |
| [str(venv_python), "-c", "import sys; print(sys.prefix)"], |
| capture_output=True, |
| text=True, |
| check=True, |
| ) |
| python_prefix = python_prefix_result.stdout.strip() |
| print(f"Using Python from: {python_prefix}") |
| |
| # Ensure PyInstaller runs from the virtual environment |
| # The venv_python should automatically set up the correct environment |
| cmd = [ |
| str(venv_python), |
| "-m", |
| "PyInstaller", |
| "--noconfirm", |
| str(spec_file), |
| ] |
| |
| try: |
| subprocess.run(cmd, check=True, env=pyinstaller_env) |
| except subprocess.CalledProcessError as e: |
| print(f"\nError: Build failed: {e}") |
| sys.exit(1) |
| |
| print() |
| print("=" * 50) |
| print("Build completed!") |
| print("=" * 50) |
| print() |
| print("Executable location: dist/ainode/ainode") |
| print() |
| print("Usage:") |
| print(" ./dist/ainode/ainode start # Start AINode") |
| print() |
| |
| |
| if __name__ == "__main__": |
| build() |