Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion framework/py/flwr/common/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,22 @@
from os.path import isfile
from pathlib import Path

from flwr.common.constant import TRANSPORT_TYPE_REST
from flwr.common.constant import RUNTIME_DEPENDENCY_INSTALL, TRANSPORT_TYPE_REST
from flwr.common.logger import log


def add_args_runtime_dependency_install(parser: argparse.ArgumentParser) -> None:
"""Add arguments controlling runtime dependency installation."""
parser.add_argument(
"--allow-runtime-dependency-installation",
action="store_true",
dest="runtime_dependency_install",
default=RUNTIME_DEPENDENCY_INSTALL,
help="Allow runtime installation of app dependencies via `uv sync`. "
"By default, runtime dependency installation is disabled.",
)


def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
"""Add common Flower arguments for flwr-*app to the provided parser."""
parser.add_argument(
Expand Down Expand Up @@ -53,6 +65,7 @@ def add_args_flwr_app_common(parser: argparse.ArgumentParser) -> None:
action="store_true",
help="This flag is deprecated and will be removed in a future release.",
)
add_args_runtime_dependency_install(parser)


def try_obtain_root_certificates(
Expand Down
41 changes: 41 additions & 0 deletions framework/py/flwr/common/args_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2026 Flower Labs GmbH. All Rights Reserved.
#
# Licensed 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.
# ==============================================================================
"""Tests for runtime dependency installation CLI arguments."""


import argparse

from flwr.common.args import add_args_runtime_dependency_install
from flwr.common.constant import RUNTIME_DEPENDENCY_INSTALL


def test_runtime_dependency_install_args_defaults() -> None:
"""Verify runtime dependency installation args default values."""
parser = argparse.ArgumentParser()
add_args_runtime_dependency_install(parser)

args = parser.parse_args([])

assert args.runtime_dependency_install is RUNTIME_DEPENDENCY_INSTALL


def test_runtime_dependency_install_args_flags() -> None:
"""Verify runtime dependency installation args parse correctly."""
parser = argparse.ArgumentParser()
add_args_runtime_dependency_install(parser)

args = parser.parse_args(["--allow-runtime-dependency-installation"])

assert args.runtime_dependency_install is True
3 changes: 3 additions & 0 deletions framework/py/flwr/common/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
ISOLATION_MODE_SUBPROCESS = "subprocess"
ISOLATION_MODE_PROCESS = "process"

# Runtime dependency installation toggle
RUNTIME_DEPENDENCY_INSTALL = False

# Log streaming configurations
CONN_REFRESH_PERIOD = 60 # Stream connection refresh period
CONN_RECONNECT_INTERVAL = 0.5 # Reconnect interval between two stream connections
Expand Down
8 changes: 7 additions & 1 deletion framework/py/flwr/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
import yaml

from flwr.common import GRPC_MAX_MESSAGE_LENGTH, EventType, event
from flwr.common.args import try_obtain_server_certificates
from flwr.common.args import (
add_args_runtime_dependency_install,
try_obtain_server_certificates,
)
from flwr.common.constant import (
AUTHN_TYPE_YAML_KEY,
AUTHZ_TYPE_YAML_KEY,
Expand Down Expand Up @@ -437,6 +440,8 @@ def run_superlink() -> None:
ExecPluginType.SIMULATION if is_simulation else ExecPluginType.SERVER_APP,
]
command += ["--parent-pid", str(os.getpid())]
if args.runtime_dependency_install:
command += ["--allow-runtime-dependency-installation"]
# pylint: disable-next=consider-using-with
subprocess.Popen(command)

Expand Down Expand Up @@ -747,6 +752,7 @@ def _add_args_common(parser: argparse.ArgumentParser) -> None:
action="store_true",
help="Enable supernode authentication.",
)
add_args_runtime_dependency_install(parser)
parser.add_argument(
"--log-file",
type=str,
Expand Down
35 changes: 35 additions & 0 deletions framework/py/flwr/server/serverapp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
get_project_dir,
)
from flwr.common.constant import (
RUNTIME_DEPENDENCY_INSTALL,
SERVERAPPIO_API_DEFAULT_CLIENT_ADDRESS,
ExecPluginType,
Status,
Expand Down Expand Up @@ -66,6 +67,10 @@
from flwr.server.run_serverapp import run as run_
from flwr.supercore.app_utils import start_parent_process_monitor
from flwr.supercore.heartbeat import HeartbeatSender, make_app_heartbeat_fn_grpc
from flwr.supercore.superexec.dependency_installer import (
cleanup_app_runtime_environment,
install_app_dependencies,
)
from flwr.supercore.superexec.plugin import ServerAppExecPlugin
from flwr.supercore.superexec.run_superexec import run_with_deprecation_warning

Expand Down Expand Up @@ -94,6 +99,7 @@ def flwr_serverapp() -> None:
appio_api_address=args.serverappio_api_address,
parent_pid=args.parent_pid,
warn_run_once=args.run_once,
runtime_dependency_install=args.runtime_dependency_install,
)
return

Expand All @@ -110,6 +116,7 @@ def flwr_serverapp() -> None:
token=args.token,
certificates=None,
parent_pid=args.parent_pid,
runtime_dependency_install=args.runtime_dependency_install,
)

# Restore stdout/stderr
Expand All @@ -122,6 +129,7 @@ def run_serverapp( # pylint: disable=R0913, R0914, R0915, R0917, W0212
token: str,
certificates: bytes | None = None,
parent_pid: int | None = None,
runtime_dependency_install: bool = RUNTIME_DEPENDENCY_INSTALL,
) -> None:
"""Run Flower ServerApp process."""
# Monitor the main process in case of SIGKILL
Expand All @@ -136,6 +144,7 @@ def run_serverapp( # pylint: disable=R0913, R0914, R0915, R0917, W0212
heartbeat_sender = None
grid = None
context = None
runtime_env_dir: Path | None = None
exit_code = ExitCode.SUCCESS

def on_exit() -> None:
Expand Down Expand Up @@ -165,6 +174,9 @@ def on_exit() -> None:
if grid:
grid.close()

# Clean up run-scoped runtime environment, if any.
cleanup_app_runtime_environment(runtime_env_dir)

# Register signal handlers for graceful shutdown
register_signal_handlers(
event_type=EventType.FLWR_SERVERAPP_RUN_LEAVE,
Expand Down Expand Up @@ -210,6 +222,29 @@ def on_exit() -> None:
fab_id, fab_version = get_fab_metadata(fab.content)

app_path = str(get_project_dir(fab_id, fab_version, fab.hash_str))

if runtime_dependency_install:
log(DEBUG, "[flwr-serverapp] Installing app dependencies.")
runtime_env_dir = install_app_dependencies(
app_path,
launch_id=token,
run_id=run.run_id,
index_context={
"component": "serverapp",
"project_dir": app_path,
"run_id": run.run_id,
"launch_id": token,
"fab_id": run.fab_id,
"fab_version": run.fab_version,
"fab_hash": fab.hash_str,
},
)
else:
log(
DEBUG,
"[flwr-serverapp] Runtime dependency installation is disabled.",
)

config = get_project_config(app_path)

# Obtain server app reference and the run config
Expand Down
32 changes: 32 additions & 0 deletions framework/py/flwr/simulation/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
unflatten_dict,
)
from flwr.common.constant import (
RUNTIME_DEPENDENCY_INSTALL,
SIMULATIONIO_API_DEFAULT_CLIENT_ADDRESS,
ExecPluginType,
Status,
Expand Down Expand Up @@ -69,6 +70,10 @@
from flwr.simulation.simulationio_connection import SimulationIoConnection
from flwr.supercore.app_utils import start_parent_process_monitor
from flwr.supercore.heartbeat import HeartbeatSender, make_app_heartbeat_fn_grpc
from flwr.supercore.superexec.dependency_installer import (
cleanup_app_runtime_environment,
install_app_dependencies,
)
from flwr.supercore.superexec.plugin import SimulationExecPlugin
from flwr.supercore.superexec.run_superexec import run_with_deprecation_warning

Expand Down Expand Up @@ -97,6 +102,7 @@ def flwr_simulation() -> None:
appio_api_address=args.simulationio_api_address,
parent_pid=args.parent_pid,
warn_run_once=args.run_once,
runtime_dependency_install=args.runtime_dependency_install,
)
return

Expand All @@ -113,6 +119,7 @@ def flwr_simulation() -> None:
token=args.token,
certificates=None,
parent_pid=args.parent_pid,
runtime_dependency_install=args.runtime_dependency_install,
)

# Restore stdout/stderr
Expand All @@ -125,6 +132,7 @@ def run_simulation_process( # pylint: disable=R0913, R0914, R0915, R0917, W0212
token: str,
certificates: bytes | None = None,
parent_pid: int | None = None,
runtime_dependency_install: bool = RUNTIME_DEPENDENCY_INSTALL,
) -> None:
"""Run Flower Simulation process."""
# Start monitoring the parent process if a PID is provided
Expand All @@ -142,6 +150,8 @@ def run_simulation_process( # pylint: disable=R0913, R0914, R0915, R0917, W0212
heartbeat_sender = None
run = None
run_status = None
run_id_hash = None
runtime_env_dir = None
exit_code = ExitCode.SUCCESS

def on_exit() -> None:
Expand All @@ -160,6 +170,8 @@ def on_exit() -> None:
UpdateRunStatusRequest(run_id=run.run_id, run_status=run_status_proto)
)

cleanup_app_runtime_environment(runtime_env_dir)

register_signal_handlers(
event_type=EventType.FLWR_SIMULATION_RUN_LEAVE,
exit_message="Run stopped by user.",
Expand Down Expand Up @@ -188,6 +200,26 @@ def on_exit() -> None:
fab_id, fab_version = get_fab_metadata(fab.content)

app_path = get_project_dir(fab_id, fab_version, fab.hash_str)

if runtime_dependency_install:
log(DEBUG, "Simulation process starts app dependency installation.")
runtime_env_dir = install_app_dependencies(
app_path,
launch_id=token,
run_id=run.run_id,
index_context={
"component": "simulation",
"project_dir": str(app_path),
"run_id": run.run_id,
"launch_id": token,
"fab_id": run.fab_id,
"fab_version": run.fab_version,
"fab_hash": fab.hash_str,
},
)
else:
log(DEBUG, "Simulation runtime dependency installation is disabled.")

config = get_project_config(app_path)

# Get ClientApp and SeverApp components
Expand Down
3 changes: 3 additions & 0 deletions framework/py/flwr/supercore/cli/flower_superexec.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import yaml

from flwr.common import EventType, event
from flwr.common.args import add_args_runtime_dependency_install
from flwr.common.constant import ExecPluginType
from flwr.common.exit import ExitCode, flwr_exit
from flwr.common.logger import log
Expand Down Expand Up @@ -101,6 +102,7 @@ def flower_superexec() -> None:
plugin_config=plugin_config,
parent_pid=args.parent_pid,
health_server_address=args.health_server_address,
runtime_dependency_install=args.runtime_dependency_install,
)


Expand Down Expand Up @@ -135,6 +137,7 @@ def _parse_args() -> argparse.ArgumentParser:
)
add_ee_args_superexec(parser)
add_args_health(parser)
add_args_runtime_dependency_install(parser)
return parser


Expand Down
Loading
Loading