Skip to content

Commit 82adee8

Browse files
committed
Bootstrap command. Fix 'create env' and 'install deps in env' handlers after migration to resource uri
1 parent 7d6e625 commit 82adee8

15 files changed

Lines changed: 328 additions & 75 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,8 @@ jobs:
5555

5656
- name: Install dependencies
5757
run: |
58-
python -m venv .venvs/dev_workspace
58+
pipx run finecode bootstrap
5959
source .venvs/dev_workspace/bin/activate
60-
python -m pip install --upgrade pip==25.3
61-
62-
# split installation into 2 steps: first internal preset because it is not
63-
# published as pypi package, so pip will fail to resolve it in dev_workspace
64-
# group and only then other dependencies
65-
python -m pip install -e ./finecode_dev_common_preset
66-
python -m pip install --group="dev_workspace" -e . -e ./finecode_extension_runner
6760
6861
python -m finecode prepare-envs
6962
shell: bash

docs/getting-started.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ This guide walks you through installing FineCode, applying an existing preset, a
44

55
## Prerequisites
66

7-
- Python 3.11–3.14
8-
- pip 25.1 or newer (for `--group` support)
7+
- Python 3.11–3.14 **or** [uv](https://docs.astral.sh/uv/) (which can install Python for you)
8+
9+
No Python yet? Install `uv` (a single binary, no Python needed):
910

1011
```bash
11-
python -m pip install --upgrade pip
12+
# Linux / macOS
13+
curl -LsSf https://astral.sh/uv/install.sh | sh
14+
15+
# Windows
16+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
1217
```
1318

1419
## 1. Add FineCode to your project
@@ -22,7 +27,19 @@ Add the `dev_workspace` dependency group to your `pyproject.toml`:
2227
dev_workspace = ["finecode==0.3.*"]
2328
```
2429

25-
Create the venv and install:
30+
Bootstrap the `dev_workspace` environment:
31+
32+
```bash
33+
# Recommended — works with pipx (bundled with Python 3.13+) or uv:
34+
pipx run finecode bootstrap
35+
# or
36+
uvx finecode bootstrap
37+
```
38+
39+
This creates `.venvs/dev_workspace/` with FineCode installed, using the exact
40+
versions specified in your `pyproject.toml`.
41+
42+
**Manual alternative** (if you prefer not to use pipx/uvx — requires pip 25.1+):
2643

2744
```bash
2845
python -m venv .venvs/dev_workspace

docs/guides/preparing-environments.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,37 @@ After this step every handler has all its dependencies available and can execute
3737

3838
The `dev_workspace` env is special: it contains FineCode itself and the preset packages. The handlers that implement `create_envs` and `install_envs` live inside `dev_workspace` — which creates a bootstrapping constraint.
3939

40-
### Workspace root bootstrap (manual, one-time)
40+
### Workspace root bootstrap (one-time)
4141

42-
The workspace root's `dev_workspace` is the **seed** for everything. `prepare-envs` cannot run unless FineCode is already installed somewhere, so the workspace root's `dev_workspace` must be created manually on a fresh checkout:
42+
The workspace root's `dev_workspace` is the **seed** for everything. `prepare-envs` cannot run unless FineCode is already installed somewhere, so the workspace root's `dev_workspace` must be created before `prepare-envs` can run.
43+
44+
Use the `bootstrap` command — it handles this automatically using the invoking Python (e.g. the pipx/uvx ephemeral environment):
45+
46+
```bash
47+
# With pipx (bundled with Python 3.13+):
48+
pipx run finecode bootstrap
49+
50+
# With uv (also works when you have no Python — uv installs it):
51+
uvx finecode bootstrap
52+
```
53+
54+
> **Note:** `bootstrap` uses the built-in default handlers: `virtualenv` for environment creation and `pip` for dependency installation. If your project requires custom handlers for either action (e.g. a different package manager or venv backend), `bootstrap` is not suitable — you must bootstrap the `dev_workspace` manually (see the **Manual alternative** below) or via your own tooling.
55+
56+
To delete and recreate an existing `dev_workspace`:
57+
58+
```bash
59+
pipx run finecode bootstrap --recreate
60+
```
61+
62+
**Manual alternative** (requires pip 25.1+):
4363

4464
```bash
4565
python -m venv .venvs/dev_workspace
4666
source .venvs/dev_workspace/bin/activate # Windows: .venvs\dev_workspace\Scripts\activate
4767
python -m pip install --group="dev_workspace"
4868
```
4969

50-
This is the only step that cannot be automated by FineCode itself. See [Getting Started](../getting-started.md) for the full first-time setup sequence.
70+
See [Getting Started](../getting-started.md) for the full first-time setup sequence.
5171

5272
### Subproject bootstrap (automated by `prepare-envs`)
5373

extensions/fine_python_pip/src/fine_python_pip/install_deps_in_env_handler.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from finecode_extension_api import code_action
55
from finecode_extension_api.actions.environments import install_deps_in_env_action
66
from finecode_extension_api.interfaces import icommandrunner, ilogger
7+
from finecode_extension_api.resource_uri import resource_uri_to_path
78

89

910
@dataclasses.dataclass
@@ -35,8 +36,8 @@ async def run(
3536
) -> install_deps_in_env_action.InstallDepsInEnvRunResult:
3637
env_name = payload.env_name
3738
dependencies = payload.dependencies
38-
venv_dir_path = payload.venv_dir_path
39-
project_dir_path = payload.project_dir_path
39+
venv_dir_path = resource_uri_to_path(payload.venv_dir_path)
40+
project_dir_path = resource_uri_to_path(payload.project_dir_path)
4041
python_executable = venv_dir_path / "bin" / "python"
4142

4243
cmd = self._construct_pip_install_cmd(

extensions/fine_python_virtualenv/src/fine_python_virtualenv/create_env_handler.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from finecode_extension_api.actions.environments import create_env_action
77
from finecode_extension_api.actions.environments.create_envs_action import CreateEnvsRunResult
88
from finecode_extension_api.interfaces import ifilemanager, ilogger
9+
from finecode_extension_api.resource_uri import resource_uri_to_path
910

1011

1112
@dataclasses.dataclass
@@ -33,23 +34,29 @@ async def run(
3334
run_context: create_env_action.CreateEnvRunContext,
3435
) -> CreateEnvsRunResult:
3536
env_info = payload.env
36-
if payload.recreate and env_info.venv_dir_path.exists():
37-
self.logger.debug(f"Remove virtualenv dir {env_info.venv_dir_path}")
38-
await self.file_manager.remove_dir(env_info.venv_dir_path)
37+
venv_dir_path = resource_uri_to_path(env_info.venv_dir_path)
38+
if payload.recreate and venv_dir_path.exists():
39+
self.logger.debug(f"Remove virtualenv dir {venv_dir_path}")
40+
await self.file_manager.remove_dir(venv_dir_path)
3941

40-
self.logger.info(f"Creating virtualenv {env_info.venv_dir_path}")
41-
if not env_info.venv_dir_path.exists():
42+
self.logger.info(f"Creating virtualenv {venv_dir_path}")
43+
# Check for pyvenv.cfg rather than the directory itself — the runner
44+
# may have already created a logs/ subdirectory inside this path before
45+
# the venv is set up, which would cause a directory-existence check to
46+
# incorrectly skip venv creation.
47+
venv_valid = (venv_dir_path / "pyvenv.cfg").exists()
48+
if not venv_valid:
4249
try:
4350
virtualenv.cli_run(
44-
[env_info.venv_dir_path.as_posix()],
51+
[venv_dir_path.as_posix()],
4552
options=None,
4653
setup_logging=False,
4754
env=None,
4855
)
4956
except Exception as exc:
5057
return CreateEnvsRunResult(
5158
errors=[
52-
f"Failed to create virtualenv {env_info.venv_dir_path}: {exc}"
59+
f"Failed to create virtualenv {venv_dir_path}: {exc}"
5360
]
5461
)
5562
else:

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ dependencies = [
2424
"apischema==0.19.*",
2525
]
2626

27+
[project.scripts]
28+
finecode = "finecode.cli:cli"
29+
2730
[dependency-groups]
2831
dev_workspace = [
2932
"build==1.2.2.post1",

src/finecode/cli.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,43 @@ def prepare_envs(log_level: str, debug: bool, recreate: bool, shared_server: boo
461461
sys.exit(2)
462462

463463

464+
@cli.command()
465+
@click.option("--recreate", is_flag=True, default=False,
466+
help="Delete and recreate dev_workspace if it already exists.")
467+
@click.option("--log-level", "log_level", default="INFO",
468+
type=click.Choice(["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"],
469+
case_sensitive=False), show_default=True)
470+
def bootstrap(recreate: bool, log_level: str) -> None:
471+
"""Create the dev_workspace environment for the workspace root.
472+
473+
Intended as a one-time setup step before running prepare-envs.
474+
Can be run via ``pipx run finecode bootstrap`` or ``uvx finecode bootstrap``
475+
without a pre-existing virtualenv.
476+
"""
477+
import asyncio
478+
479+
from finecode.cli_app.commands import bootstrap_cmd
480+
481+
logger_utils.init_logger(log_name="cli", log_level=log_level, stdout=True)
482+
user_messages._notification_sender = show_user_message
483+
484+
try:
485+
asyncio.run(
486+
bootstrap_cmd.bootstrap(
487+
workdir_path=pathlib.Path(os.getcwd()),
488+
recreate=recreate,
489+
log_level=log_level,
490+
)
491+
)
492+
except bootstrap_cmd.BootstrapFailed as exception:
493+
click.echo(exception.message, err=True)
494+
sys.exit(1)
495+
except Exception as exception:
496+
logger.exception(exception)
497+
click.echo("Unexpected error, see logs in file for more details", err=True)
498+
sys.exit(2)
499+
500+
464501
@cli.command()
465502
@click.option("--log-level", "log_level", default="INFO", type=click.Choice(["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), show_default=True)
466503
@click.option("--debug", "debug", is_flag=True, default=False)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Shared helper for create_envs + install_envs used by bootstrap and prepare-envs."""
2+
from finecode.wm_client import ApiClient, ApiError
3+
4+
5+
class EnvSetupFailed(Exception):
6+
def __init__(self, message: str) -> None:
7+
self.message = message
8+
9+
10+
async def create_and_install_envs(
11+
client: ApiClient,
12+
project_path: str,
13+
envs: list[dict],
14+
dev_env: str = "cli",
15+
) -> None:
16+
"""Run ``create_envs`` then ``install_envs`` for the given environments.
17+
18+
Raises :class:`EnvSetupFailed` on any failure so callers can re-raise
19+
with their own exception type.
20+
"""
21+
options = {
22+
"resultFormats": ["string"],
23+
"trigger": "user",
24+
"devEnv": dev_env,
25+
}
26+
27+
try:
28+
create_result = await client.run_action(
29+
action="create_envs",
30+
project=project_path,
31+
params={"envs": envs},
32+
options=options,
33+
)
34+
except ApiError as exc:
35+
raise EnvSetupFailed(f"'create_envs' failed: {exc}") from exc
36+
if create_result.get("returnCode", 0) != 0:
37+
output = (create_result.get("resultByFormat") or {}).get("string", "")
38+
raise EnvSetupFailed(
39+
f"'create_envs' failed with return code "
40+
f"{create_result['returnCode']}: {output}"
41+
)
42+
43+
try:
44+
install_result = await client.run_action(
45+
action="install_envs",
46+
project=project_path,
47+
params={"envs": envs},
48+
options=options,
49+
)
50+
except ApiError as exc:
51+
raise EnvSetupFailed(f"'install_envs' failed: {exc}") from exc
52+
if install_result.get("returnCode", 0) != 0:
53+
output = (install_result.get("resultByFormat") or {}).get("string", "")
54+
raise EnvSetupFailed(
55+
f"'install_envs' failed with return code "
56+
f"{install_result['returnCode']}: {output}"
57+
)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# docs: docs/cli.md
2+
import pathlib
3+
import sys
4+
5+
from finecode.wm_client import ApiClient, ApiError # ApiError used for start_runners check
6+
from finecode.wm_server import wm_lifecycle
7+
from finecode.cli_app.commands._env_setup import create_and_install_envs, EnvSetupFailed
8+
from loguru import logger
9+
10+
11+
class BootstrapFailed(Exception):
12+
def __init__(self, message: str) -> None:
13+
self.message = message
14+
15+
16+
async def bootstrap(
17+
workdir_path: pathlib.Path,
18+
recreate: bool = False,
19+
log_level: str = "INFO",
20+
) -> None:
21+
"""Create the dev_workspace environment for the workspace root.
22+
23+
Uses the current Python process (sys.executable) as a temporary runner so
24+
that ``create_envs`` and ``install_envs`` can run before the venv exists.
25+
After bootstrap, run ``finecode prepare-envs`` to set up all other envs.
26+
"""
27+
port_file = None
28+
try:
29+
port_file = wm_lifecycle.start_own_server(workdir_path, log_level=log_level)
30+
try:
31+
port = await wm_lifecycle.wait_until_ready_from_file(port_file)
32+
except TimeoutError as exc:
33+
raise BootstrapFailed(str(exc)) from exc
34+
35+
client = ApiClient()
36+
await client.connect("127.0.0.1", port)
37+
try:
38+
await _run(client, workdir_path, recreate)
39+
finally:
40+
await client.close()
41+
finally:
42+
if port_file is not None and port_file.exists():
43+
port_file.unlink(missing_ok=True)
44+
45+
46+
async def _run(
47+
client: ApiClient,
48+
workdir_path: pathlib.Path,
49+
recreate: bool,
50+
) -> None:
51+
venv_dir = workdir_path / ".venvs" / "dev_workspace"
52+
workdir_str = str(workdir_path)
53+
54+
if venv_dir.exists() and not recreate:
55+
logger.info(
56+
f"dev_workspace already exists at '{venv_dir}'. "
57+
"Use --recreate to delete and recreate it."
58+
)
59+
return
60+
61+
# Discover projects (no runners — venv may not exist yet).
62+
logger.info("Discovering projects...")
63+
result = await client.add_dir(workdir_path, start_runners=False)
64+
projects: list[dict] = result.get("projects", [])
65+
66+
current_project = next((p for p in projects if p["path"] == workdir_str), None)
67+
if current_project is None:
68+
raise BootstrapFailed(
69+
"bootstrap must be run from the workspace/project root"
70+
)
71+
if current_project["status"] == "CONFIG_INVALID":
72+
raise BootstrapFailed(
73+
f"Project '{current_project['name']}' has invalid configuration"
74+
)
75+
76+
if venv_dir.exists():
77+
# recreate=True (already handled False above)
78+
logger.info(f"Removing existing dev_workspace at '{venv_dir}'...")
79+
try:
80+
await client.remove_env(workdir_str, "dev_workspace")
81+
except ApiError as exc:
82+
raise BootstrapFailed(
83+
f"Failed to remove existing dev_workspace: {exc}"
84+
) from exc
85+
86+
# Start the dev_workspace runner using the current Python executable.
87+
# This works even though the venv doesn't exist yet because sys.executable
88+
# already has finecode and all handlers installed (e.g. via pipx/uvx).
89+
logger.info(f"Starting temporary runner using {sys.executable}...")
90+
try:
91+
await client.start_runners(
92+
projects=[workdir_str],
93+
python_overrides={"dev_workspace": sys.executable},
94+
)
95+
except ApiError as exc:
96+
raise BootstrapFailed(f"Failed to start runner: {exc}") from exc
97+
98+
dw_env = {
99+
"name": "dev_workspace",
100+
"venv_dir_path": venv_dir.as_uri(),
101+
"project_def_path": (workdir_path / "pyproject.toml").as_uri(),
102+
}
103+
104+
logger.info("Creating dev_workspace virtualenv and installing dependencies...")
105+
try:
106+
await create_and_install_envs(
107+
client=client,
108+
project_path=workdir_str,
109+
envs=[dw_env],
110+
dev_env="cli",
111+
)
112+
except EnvSetupFailed as exc:
113+
raise BootstrapFailed(exc.message) from exc
114+
115+
logger.info(
116+
f"Bootstrap complete. dev_workspace created at '{venv_dir}'.\n"
117+
"Next step: run 'finecode prepare-envs' to set up all other environments."
118+
)
119+
120+
121+
__all__ = ["bootstrap", "BootstrapFailed"]

0 commit comments

Comments
 (0)