Skip to content
Draft
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
6 changes: 3 additions & 3 deletions src/karsk/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ async def _build(ctx: Context, pkg: Package, tmp: str) -> None:


async def _build_packages(ctx: Context, stop_after: Package | None = None) -> None:
for pkg in ctx.plist.packages.values():
for pkg in ctx.packages.values():
with TemporaryDirectory() as tmp:
await _build(ctx, pkg, tmp)
if pkg is stop_after:
Expand All @@ -233,7 +233,7 @@ def _build_envs(
if base is None:
base = ctx.staging

pkg = ctx.plist.packages[ctx.config.main_package]
pkg = ctx.packages[ctx.config.main_package]
path = _get_build_path(base, pkg)
if path is not None:
_build_env_for_package(path, pkg, use_final_out=use_final_out)
Expand Down Expand Up @@ -295,7 +295,7 @@ def install_all(ctx: Context) -> None:
destination = ctx.destination
destination.mkdir(parents=True, exist_ok=True)

for pkg in ctx.plist.packages.values():
for pkg in ctx.packages.values():
if not pkg.out.exists():
sys.exit(
f"Package {pkg.fullname} has not been built. Run 'karsk build' first."
Expand Down
34 changes: 26 additions & 8 deletions src/karsk/commands/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from karsk.commands._common import argument_config_file, option_staging
from karsk.config import Config, AreaConfig, load_config
from karsk.package_list import PackageList
from karsk.package_list import PackageList, create_packages
from karsk.utils import redirect_output


Expand All @@ -28,21 +28,22 @@ class Sync:

def __init__(
self,
plist: PackageList,
config: Config,
packages: PackageList,
*,
dry_run: bool = False,
) -> None:
self._destination: Path = plist.config.destination
self._storepath: Path = plist.config.destination / ".store"
self._destination: Path = config.destination
self._storepath: Path = config.destination / ".store"
self._dry_run: bool = dry_run

self._store_paths: list[Path] = [pkg.out for pkg in plist.packages.values()]
self._store_paths: list[Path] = [pkg.out for pkg in packages.values()]

self._env_paths: list[Path] = [
path.parent
for path in self._destination.glob("*/manifest")
if not path.parent.is_symlink()
if plist.packages[plist.config.main_package].manifest == path.read_text()
if packages[config.main_package].manifest == path.read_text()
]

# Create preliminary script
Expand Down Expand Up @@ -152,14 +153,31 @@ async def _check_call(
)


def _check_existence(packages: PackageList) -> None:
for pkg in packages:
if not pkg.out.is_dir():
sys.exit(
f"{pkg.out} doesn't exist. Are you sure that '{pkg.fullname}' is installed?"
)


async def sync_all(
config: Config,
staging: Path,
no_async: bool,
dry_run: bool,
) -> None:
plist = PackageList(config, staging=staging)
syncer = Sync(plist, dry_run=dry_run)
staging_storepath = staging / ".store"
staging_storepath.mkdir(parents=True, exist_ok=True)

packages = create_packages(
config,
staging_storepath=staging_storepath,
final_storepath=config.destination / ".store",
cache=staging / "cache",
)
_check_existence(packages)
syncer = Sync(config, packages, dry_run=dry_run)

if no_async:
for area in config.areas:
Expand Down
4 changes: 2 additions & 2 deletions src/karsk/commands/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ def subcommand_test(config_file: Path, args: tuple[str, ...]) -> None:
if not testpath.is_dir():
sys.exit(f"Test directory '{testpath}' doesn't exist or is not a directory")

newpath = ":".join(str(p.out / "bin") for p in context.plist.packages.values())
newpath = ":".join(str(p.out / "bin") for p in context.packages.values())
os.environ["PATH"] = f"{newpath}:{os.environ['PATH']}"

for pkg in context.plist.packages.values():
for pkg in context.packages.values():
os.environ[f"{pkg.config.name}_version"] = pkg.config.version

print(f"{os.environ['PATH']=}")
Expand Down
41 changes: 27 additions & 14 deletions src/karsk/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from karsk.config import Config, load_config
from karsk.engine import Engine, EngineName, VolumeBind, get_engine
from karsk.package import Package
from karsk.package_list import PackageList
from karsk.package_list import PackageList, create_packages
from karsk.console import console


Expand All @@ -19,29 +19,42 @@ def __init__(
engine: EngineName | None = None,
) -> None:
self.config: Config = config
self.plist: PackageList = PackageList(
config,
staging=staging.absolute(),
check_existence=False,
)
self._staging: Path = staging.absolute()
self.engine: Engine = get_engine(engine)
self.engine_name: EngineName | None = engine

staging_storepath = self._staging / ".store"
staging_storepath.mkdir(parents=True, exist_ok=True)

self.packages: PackageList = create_packages(
config,
staging_storepath=staging_storepath,
final_storepath=config.destination / ".store",
cache=self._staging / "cache",
)

@property
def destination(self) -> Path:
return self.config.destination

@property
def staging(self) -> Path:
return self.plist.staging

@property
def packages(self) -> dict[str, Package]:
return self.plist.packages
return self._staging

def __getitem__(self, key: str) -> Package:
return self.packages[key]

def volumes(self, package_names: list[str]) -> list[VolumeBind]:
pnames = set(package_names)
for pname in package_names:
pkg = self.packages[pname]
pnames |= set(p.config.name for p in pkg.depends)

return [
(pkg.out, pkg.final_out, "ro")
for pkg in (self.packages[pname] for pname in pnames)
]

@classmethod
def from_config_file(
cls,
Expand Down Expand Up @@ -98,13 +111,13 @@ async def run(
image = self.config.build_image

if package is None:
package = sorted(self.plist.packages.keys())
package = sorted(self.packages.keys())
elif isinstance(package, str):
package = [package]

missing: list[str] = []
for pname in package:
if (pkg := self.plist.packages.get(pname)) is None:
if (pkg := self.packages.get(pname)) is None:
raise ValueError(f"No package {pname} defined")

if not pkg.is_built:
Expand All @@ -122,7 +135,7 @@ async def run(
image,
program,
*args,
volumes=volumes + self.plist.volumes(package),
volumes=volumes + self.volumes(package),
cwd=cwd,
env=env,
terminal=terminal,
Expand Down
134 changes: 71 additions & 63 deletions src/karsk/package_list.py
Original file line number Diff line number Diff line change
@@ -1,76 +1,84 @@
from __future__ import annotations

from collections.abc import Iterator
from itertools import chain
import sys
from pathlib import Path
import networkx as nx

from karsk.config import Config
from karsk.engine import VolumeBind
from karsk.package import Package


class PackageList:
def __init__(
self,
config: Config,
*,
staging: Path,
check_existence: bool = True,
) -> None:
self.staging: Path = staging
self.staging_storepath: Path = staging / Path(".store")
self.storepath: Path = config.destination / ".store"
self.config: Config = config
buildmap = {x.name: x for x in config.packages}

self.staging_storepath.mkdir(parents=True, exist_ok=True)

graph: nx.DiGraph[str] = nx.DiGraph()
for package in config.packages:
graph.add_node(package.name)
for dep in package.depends:
graph.add_edge(dep, package.name)

transitive_depends: dict[Package, list[Package]] = {}
self.packages: dict[str, Package] = {}
for node in nx.topological_sort(graph):
build = buildmap[node]

direct_depends = [self.packages[x] for x in build.depends]
node_depends = [
*direct_depends,
*chain.from_iterable(transitive_depends[x] for x in direct_depends),
]

new_package = Package(
self.staging_storepath,
self.storepath,
build,
node_depends,
config.build_image,
staging / "cache",
)
transitive_depends[new_package] = node_depends
self.packages[node] = new_package

if check_existence:
self._check_existence()

def volumes(self, package_names: list[str]) -> list[VolumeBind]:
pnames = set(package_names)
for pname in package_names:
pkg = self.packages[pname]
pnames |= set(p.config.name for p in pkg.depends)

return [
(pkg.out, pkg.final_out, "ro")
for pkg in (self.packages[pname] for pname in pnames)
"""An ordered collection of packages sorted in topological (build) order."""

def __init__(self, packages: dict[str, Package]) -> None:
self._packages = packages

def __iter__(self) -> Iterator[Package]:
return iter(self._packages.values())

def __getitem__(self, key: str) -> Package:
return self._packages[key]

def __len__(self) -> int:
return len(self._packages)

def __contains__(self, key: str) -> bool:
return key in self._packages

def get(self, key: str) -> Package | None:
return self._packages.get(key)

def keys(self) -> Iterator[str]:
return iter(self._packages.keys())

def values(self) -> Iterator[Package]:
return iter(self._packages.values())

def __eq__(self, other: object) -> bool:
if isinstance(other, PackageList):
return self._packages == other._packages
if isinstance(other, dict):
return self._packages == other
return NotImplemented


def create_packages(
config: Config,
*,
staging_storepath: Path,
final_storepath: Path,
cache: Path,
) -> PackageList:
buildmap = {x.name: x for x in config.packages}

graph: nx.DiGraph[str] = nx.DiGraph()
for package in config.packages:
graph.add_node(package.name)
for dep in package.depends:
graph.add_edge(dep, package.name)

transitive_depends: dict[Package, list[Package]] = {}
packages: dict[str, Package] = {}
for node in nx.topological_sort(graph):
build = buildmap[node]

direct_depends = [packages[x] for x in build.depends]
node_depends = [
*direct_depends,
*chain.from_iterable(transitive_depends[x] for x in direct_depends),
]

def _check_existence(self) -> None:
for pkg in self.packages.values():
if not pkg.out.is_dir():
sys.exit(
f"{pkg.out} doesn't exist. Are you sure that '{pkg.fullname}' is installed?"
)
new_package = Package(
staging_storepath,
final_storepath,
build,
node_depends,
config.build_image,
cache,
)
transitive_depends[new_package] = node_depends
packages[node] = new_package

return PackageList(packages)
6 changes: 3 additions & 3 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def test_build_with_non_local_prefix(tmp_path, base_config):
base_config, cwd=tmp_path, staging=staging, engine="native"
)

pkg = ctx.plist.packages["test"]
pkg = ctx.packages["test"]
pkg.out.mkdir(parents=True)
(pkg.out / "bin").mkdir()
(pkg.out / "bin" / "hello").write_text("#!/bin/bash\necho hello\n")
Expand Down Expand Up @@ -336,7 +336,7 @@ def test_install_appends_build_id_when_manifest_differs(tmp_path, base_config):
ctx1 = Context.from_config(
base_config, cwd=tmp_path, staging=staging, engine="native"
)
pkg1 = ctx1.plist.packages["test"]
pkg1 = ctx1.packages["test"]
pkg1.out.mkdir(parents=True)
(pkg1.out / "bin").mkdir()
(pkg1.out / "bin" / "hello").write_text("v1")
Expand All @@ -356,7 +356,7 @@ def test_install_appends_build_id_when_manifest_differs(tmp_path, base_config):
ctx2 = Context.from_config(
base_config, cwd=tmp_path, staging=staging, engine="native"
)
pkg2 = ctx2.plist.packages["test"]
pkg2 = ctx2.packages["test"]
pkg2.out.mkdir(parents=True)
(pkg2.out / "bin").mkdir()
(pkg2.out / "bin" / "hello").write_text("v2")
Expand Down
Loading