diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index bae5188fa..94347ad90 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -56,8 +56,7 @@ jobs: INFERENCE_MODEL: "meta-llama/Llama-3.2-3B-Instruct" run: | source .venv/bin/activate - # TODO: use "llama stack run" - nohup uv run python -m llama_stack.distribution.server.server --yaml-config ./llama_stack/templates/ollama/run.yaml > server.log 2>&1 & + nohup uv run llama stack run ./llama_stack/templates/ollama/run.yaml --image-type venv > server.log 2>&1 & - name: Wait for Llama Stack server to be ready run: | diff --git a/.github/workflows/providers-build.yml b/.github/workflows/providers-build.yml index be4298a98..e6871bf99 100644 --- a/.github/workflows/providers-build.yml +++ b/.github/workflows/providers-build.yml @@ -40,6 +40,7 @@ jobs: matrix: template: ${{ fromJson(needs.generate-matrix.outputs.templates) }} image-type: [venv, container] + fail-fast: false # We want to run all jobs even if some fail steps: - name: Checkout repository @@ -67,7 +68,9 @@ jobs: - name: Run Llama Stack Build run: | - uv run llama stack build --template ${{ matrix.template }} --image-type ${{ matrix.image-type }} --image-name test + # USE_COPY_NOT_MOUNT is set to true since mounting is not supported by docker buildx, we use COPY instead + # LLAMA_STACK_DIR is set to the current directory so we are building from the source + USE_COPY_NOT_MOUNT=true LLAMA_STACK_DIR=. uv run llama stack build --template ${{ matrix.template }} --image-type ${{ matrix.image-type }} --image-name test - name: Print dependencies in the image if: matrix.image-type == 'venv' diff --git a/llama_stack/cli/stack/_build.py b/llama_stack/cli/stack/_build.py index 3887bf4f9..d87e3bd0b 100644 --- a/llama_stack/cli/stack/_build.py +++ b/llama_stack/cli/stack/_build.py @@ -38,7 +38,7 @@ from llama_stack.distribution.distribution import get_provider_registry from llama_stack.distribution.resolver import InvalidProviderError from llama_stack.distribution.utils.config_dirs import DISTRIBS_BASE_DIR from llama_stack.distribution.utils.dynamic import instantiate_class_type -from llama_stack.distribution.utils.exec import formulate_run_args, run_with_pty +from llama_stack.distribution.utils.exec import formulate_run_args, run_command from llama_stack.distribution.utils.image_types import LlamaStackImageType from llama_stack.providers.datatypes import Api @@ -213,7 +213,7 @@ def run_stack_build_command(args: argparse.Namespace) -> None: config = parse_and_maybe_upgrade_config(config_dict) run_args = formulate_run_args(args.image_type, args.image_name, config, args.template) run_args.extend([run_config, str(os.getenv("LLAMA_STACK_PORT", 8321))]) - run_with_pty(run_args) + run_command(run_args) def _generate_run_config( diff --git a/llama_stack/cli/stack/run.py b/llama_stack/cli/stack/run.py index e5686fb10..57a0b28cc 100644 --- a/llama_stack/cli/stack/run.py +++ b/llama_stack/cli/stack/run.py @@ -82,7 +82,7 @@ class StackRun(Subcommand): from llama_stack.distribution.configure import parse_and_maybe_upgrade_config from llama_stack.distribution.utils.config_dirs import DISTRIBS_BASE_DIR - from llama_stack.distribution.utils.exec import formulate_run_args, run_with_pty + from llama_stack.distribution.utils.exec import formulate_run_args, run_command config_file = Path(args.config) has_yaml_suffix = args.config.endswith(".yaml") @@ -136,4 +136,4 @@ class StackRun(Subcommand): if args.tls_keyfile and args.tls_certfile: run_args.extend(["--tls-keyfile", args.tls_keyfile, "--tls-certfile", args.tls_certfile]) - run_with_pty(run_args) + run_command(run_args) diff --git a/llama_stack/distribution/build.py b/llama_stack/distribution/build.py index 0e990d129..a8ee372da 100644 --- a/llama_stack/distribution/build.py +++ b/llama_stack/distribution/build.py @@ -6,7 +6,6 @@ import importlib.resources import logging -import sys from pathlib import Path from typing import Dict, List @@ -15,7 +14,7 @@ from termcolor import cprint from llama_stack.distribution.datatypes import BuildConfig, Provider from llama_stack.distribution.distribution import get_provider_registry -from llama_stack.distribution.utils.exec import run_command, run_with_pty +from llama_stack.distribution.utils.exec import run_command from llama_stack.distribution.utils.image_types import LlamaStackImageType from llama_stack.providers.datatypes import Api @@ -123,11 +122,7 @@ def build_image( if special_deps: args.append("#".join(special_deps)) - is_terminal = sys.stdin.isatty() - if is_terminal: - return_code = run_with_pty(args) - else: - return_code = run_command(args) + return_code = run_command(args) if return_code != 0: log.error( diff --git a/llama_stack/distribution/utils/exec.py b/llama_stack/distribution/utils/exec.py index 86613dc9c..3bf3c81ce 100644 --- a/llama_stack/distribution/utils/exec.py +++ b/llama_stack/distribution/utils/exec.py @@ -4,13 +4,10 @@ # This source code is licensed under the terms described in the LICENSE file in # the root directory of this source tree. -import errno import logging import os -import select import signal import subprocess -import sys from termcolor import cprint @@ -88,13 +85,6 @@ def formulate_run_args(image_type, image_name, config, template_name) -> list: return run_args -def run_with_pty(command): - if sys.platform.startswith("win"): - return _run_with_pty_win(command) - else: - return _run_with_pty_unix(command) - - def in_notebook(): try: from IPython import get_ipython @@ -108,19 +98,19 @@ def in_notebook(): return True -# run a command in a pseudo-terminal, with interrupt handling, -# useful when you want to run interactive things -def _run_with_pty_unix(command): - import pty - import termios +def run_command(command: list[str]) -> int: + """ + Run a command with interrupt handling and output capture. + Uses subprocess.run with direct stream piping for better performance. - master, slave = pty.openpty() + Args: + command (list): The command to run. - old_settings = termios.tcgetattr(sys.stdin) + Returns: + int: The return code of the command. + """ original_sigint = signal.getsignal(signal.SIGINT) - ctrl_c_pressed = False - process = None def sigint_handler(signum, frame): nonlocal ctrl_c_pressed @@ -131,106 +121,19 @@ def _run_with_pty_unix(command): # Set up the signal handler signal.signal(signal.SIGINT, sigint_handler) - new_settings = termios.tcgetattr(sys.stdin) - new_settings[3] = new_settings[3] & ~termios.ECHO # Disable echo - new_settings[3] = new_settings[3] & ~termios.ICANON # Disable canonical mode - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings) - - process = subprocess.Popen( + # Run the command with stdout/stderr piped directly to system streams + result = subprocess.run( command, - stdin=slave, - stdout=slave, - stderr=slave, - universal_newlines=True, - preexec_fn=os.setsid, + text=True, + check=False, ) - - # Close the slave file descriptor as it's now owned by the subprocess - os.close(slave) - - def handle_io(): - while not ctrl_c_pressed: - try: - rlist, _, _ = select.select([sys.stdin, master], [], [], 0.1) - - if sys.stdin in rlist: - data = os.read(sys.stdin.fileno(), 1024) - if not data: - break - os.write(master, data) - - if master in rlist: - data = os.read(master, 1024) - if not data: - break - sys.stdout.buffer.write(data) - sys.stdout.flush() - - except KeyboardInterrupt: - # This will be raised when Ctrl+C is pressed - break - - if process.poll() is not None: - break - - handle_io() - except (EOFError, KeyboardInterrupt): - pass - except OSError as e: - if e.errno != errno.EIO: - raise - finally: - # Clean up - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) - signal.signal(signal.SIGINT, original_sigint) - - os.close(master) - if process and process.poll() is None: - process.terminate() - process.wait() - - return process.returncode - - -# run a command in a pseudo-terminal in windows, with interrupt handling, -def _run_with_pty_win(command): - """ - Runs a command with interactive support using subprocess directly. - """ - try: - # For shell scripts on Windows, use appropriate shell - if isinstance(command, (list, tuple)): - if command[0].endswith(".sh"): - if os.path.exists("/usr/bin/bash"): # WSL - command = ["bash"] + command - else: - # Use cmd.exe with bash while preserving all arguments - command = ["cmd.exe", "/c", "bash"] + command - - process = subprocess.Popen( - command, - shell=True, - universal_newlines=True, - ) - - process.wait() - + return result.returncode + except subprocess.SubprocessError as e: + log.error(f"Subprocess error: {e}") + return 1 except Exception as e: - print(f"Error: {str(e)}") + log.exception(f"Unexpected error: {e}") return 1 finally: - if process and process.poll() is None: - process.terminate() - process.wait() - return process.returncode - - -def run_command(command): - try: - result = subprocess.run(command, capture_output=True, text=True, check=True) - print("Script Output\n", result.stdout) - return result.returncode - except subprocess.CalledProcessError as e: - print("Error running script:", e) - print("Error output:", e.stderr) - return e.returncode + # Restore the original signal handler + signal.signal(signal.SIGINT, original_sigint)