diff --git a/furtka/installer.py b/furtka/installer.py index 246d6e1..f5ec1ae 100644 --- a/furtka/installer.py +++ b/furtka/installer.py @@ -171,13 +171,22 @@ def install_from(src: Path, settings: dict[str, str] | None = None) -> Path: target.mkdir(parents=True, exist_ok=True) for item in src.iterdir(): + dest = target / item.name + # Subdirectories (e.g. scripts/ holding a provider's on_install/on_start + # dependency hooks) must come along too — copy the whole tree, replacing + # any stale copy from a previous install so removed files don't linger. + if item.is_dir(): + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(item, dest) + continue if not item.is_file(): continue # Never overwrite an existing user .env — either settings-driven write # or previous manual edit has authority. if item.name == ".env" and (target / ".env").exists(): continue - shutil.copy2(item, target / item.name) + shutil.copy2(item, dest) env = target / ".env" env_example = target / ".env.example" diff --git a/tests/test_installer.py b/tests/test_installer.py index 22b171f..f784957 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -73,6 +73,31 @@ def test_install_from_copies_files(tmp_path, fake_dirs): assert (target / ".env").read_text() == "A=1" +def test_install_from_copies_subdirectories(tmp_path, fake_dirs): + # Dependency hooks live under scripts/ — the whole subtree must be copied, + # not just top-level files, or providers ship without their hooks. + src = _write_app_source(tmp_path, "mosquitto", VALID_MANIFEST, env_example="A=1") + scripts = src / "scripts" + scripts.mkdir() + (scripts / "provision-client.sh").write_text("#!/bin/sh\necho hi\n") + target = installer.install_from(src) + assert (target / "scripts" / "provision-client.sh").read_text() == "#!/bin/sh\necho hi\n" + + +def test_install_from_reinstall_drops_stale_subdir_files(tmp_path, fake_dirs): + # A reinstall whose source renamed/removed a hook must not leave the old one. + src = _write_app_source(tmp_path, "mosquitto", VALID_MANIFEST, env_example="A=1") + scripts = src / "scripts" + scripts.mkdir() + (scripts / "old-hook.sh").write_text("old\n") + installer.install_from(src) + (scripts / "old-hook.sh").unlink() + (scripts / "new-hook.sh").write_text("new\n") + target = installer.install_from(src) + assert not (target / "scripts" / "old-hook.sh").exists() + assert (target / "scripts" / "new-hook.sh").read_text() == "new\n" + + def test_install_from_preserves_existing_env(tmp_path, fake_dirs): src = _write_app_source(tmp_path, "fileshare", VALID_MANIFEST, env_example="A=new") target = apps_dir() / "fileshare"