Skip to content

Commit fb93925

Browse files
committed
feat(scanners): nix-state, version/package managers, containers
Implement 4 new scanners detecting non-macOS-native package management: - nix_state: Nix installation, profiles, nix-darwin, home-manager, channels, flakes, registries, config, devbox/devenv/direnv - version_managers: asdf, mise, nvm, pyenv, rbenv, jenv, sdkman - package_managers: MacPorts, Conda/Mamba - containers: Docker, Podman, Colima, OrbStack, Lima Also extends existing scanners: - system_scanner: Rosetta 2, System Extensions, iCloud, MDM detection - applications: 9 new BinarySource values + 19 path patterns New models in package_managers.py (27 models). SystemConfig extended with rosetta_installed, system_extensions, icloud, mdm_enrolled. SystemState wired with 4 new domain fields. 19 total scanners. 242 new tests (734 total), ruff clean, pyright 0 errors.
1 parent 87b2dd4 commit fb93925

20 files changed

Lines changed: 5272 additions & 8 deletions

src/mac2nix/models/__init__.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,35 @@
3737
Monitor,
3838
NightShiftConfig,
3939
)
40+
from mac2nix.models.package_managers import (
41+
CondaEnvironment,
42+
CondaPackage,
43+
CondaState,
44+
ContainerRuntimeInfo,
45+
ContainerRuntimeType,
46+
ContainersResult,
47+
DevboxProject,
48+
DevenvProject,
49+
HomeManagerState,
50+
MacPortsPackage,
51+
MacPortsState,
52+
ManagedRuntime,
53+
NixChannel,
54+
NixConfig,
55+
NixDarwinState,
56+
NixDirenvConfig,
57+
NixFlakeInput,
58+
NixInstallation,
59+
NixInstallType,
60+
NixProfile,
61+
NixProfilePackage,
62+
NixRegistryEntry,
63+
NixState,
64+
PackageManagersResult,
65+
VersionManagerInfo,
66+
VersionManagersResult,
67+
VersionManagerType,
68+
)
4069
from mac2nix.models.preferences import PreferencesDomain, PreferencesResult, PreferenceValue
4170
from mac2nix.models.services import (
4271
CronEntry,
@@ -50,11 +79,13 @@
5079
)
5180
from mac2nix.models.system import (
5281
FirewallAppRule,
82+
ICloudState,
5383
NetworkConfig,
5484
NetworkInterface,
5585
PrinterInfo,
5686
SecurityState,
5787
SystemConfig,
88+
SystemExtension,
5889
TimeMachineConfig,
5990
VpnProfile,
6091
)
@@ -72,8 +103,16 @@
72103
"BrewFormula",
73104
"BrewService",
74105
"BundleEntry",
106+
"CondaEnvironment",
107+
"CondaPackage",
108+
"CondaState",
75109
"ConfigFileType",
110+
"ContainerRuntimeInfo",
111+
"ContainerRuntimeType",
112+
"ContainersResult",
76113
"CronEntry",
114+
"DevboxProject",
115+
"DevenvProject",
77116
"DisplayConfig",
78117
"DotfileEntry",
79118
"DotfileManager",
@@ -83,7 +122,9 @@
83122
"FontEntry",
84123
"FontSource",
85124
"FontsResult",
125+
"HomeManagerState",
86126
"HomebrewState",
127+
"ICloudState",
87128
"InstalledApp",
88129
"KeyBindingEntry",
89130
"LaunchAgentEntry",
@@ -93,11 +134,26 @@
93134
"LibraryAuditResult",
94135
"LibraryDirEntry",
95136
"LibraryFileEntry",
137+
"MacPortsPackage",
138+
"MacPortsState",
139+
"ManagedRuntime",
96140
"MasApp",
97141
"Monitor",
98142
"NetworkConfig",
99143
"NetworkInterface",
100144
"NightShiftConfig",
145+
"NixChannel",
146+
"NixConfig",
147+
"NixDarwinState",
148+
"NixDirenvConfig",
149+
"NixFlakeInput",
150+
"NixInstallType",
151+
"NixInstallation",
152+
"NixProfile",
153+
"NixProfilePackage",
154+
"NixRegistryEntry",
155+
"NixState",
156+
"PackageManagersResult",
101157
"PathBinary",
102158
"PreferenceValue",
103159
"PreferencesDomain",
@@ -108,8 +164,12 @@
108164
"ShellConfig",
109165
"ShellFramework",
110166
"SystemConfig",
167+
"SystemExtension",
111168
"SystemState",
112169
"TimeMachineConfig",
170+
"VersionManagerInfo",
171+
"VersionManagerType",
172+
"VersionManagersResult",
113173
"VpnProfile",
114174
"WorkflowEntry",
115175
]

src/mac2nix/models/application.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,24 @@ class AppSource(StrEnum):
1515

1616

1717
class BinarySource(StrEnum):
18+
ASDF = "asdf"
1819
BREW = "brew"
1920
CARGO = "cargo"
21+
CONDA = "conda"
22+
GEM = "gem"
2023
GO = "go"
21-
PIPX = "pipx"
24+
JENV = "jenv"
25+
MACPORTS = "macports"
26+
MANUAL = "manual"
27+
MISE = "mise"
28+
NIX = "nix"
2229
NPM = "npm"
23-
GEM = "gem"
30+
NVM = "nvm"
31+
PIPX = "pipx"
32+
PYENV = "pyenv"
33+
RBENV = "rbenv"
34+
SDKMAN = "sdkman"
2435
SYSTEM = "system"
25-
MANUAL = "manual"
2636

2737

2838
class InstalledApp(BaseModel):
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"""Nix, version manager, and third-party package manager models."""
2+
3+
from __future__ import annotations
4+
5+
from enum import StrEnum
6+
from pathlib import Path
7+
8+
from pydantic import BaseModel, Field
9+
10+
11+
class NixInstallType(StrEnum):
12+
SINGLE_USER = "single_user"
13+
MULTI_USER = "multi_user"
14+
DETERMINATE = "determinate"
15+
UNKNOWN = "unknown"
16+
17+
18+
class NixInstallation(BaseModel):
19+
present: bool = False
20+
version: str | None = None
21+
store_path: Path = Path("/nix/store")
22+
install_type: NixInstallType = NixInstallType.UNKNOWN
23+
daemon_running: bool = False
24+
25+
26+
class NixProfilePackage(BaseModel):
27+
name: str
28+
version: str | None = None
29+
store_path: Path | None = None
30+
31+
32+
class NixProfile(BaseModel):
33+
name: str
34+
path: Path
35+
packages: list[NixProfilePackage] = []
36+
37+
38+
class NixDarwinState(BaseModel):
39+
present: bool = False
40+
generation: int | None = None
41+
config_path: Path | None = None
42+
system_packages: list[str] = []
43+
44+
45+
class HomeManagerState(BaseModel):
46+
present: bool = False
47+
generation: int | None = None
48+
config_path: Path | None = None
49+
packages: list[str] = []
50+
51+
52+
class NixChannel(BaseModel):
53+
name: str
54+
url: str
55+
56+
57+
class NixFlakeInput(BaseModel):
58+
name: str
59+
url: str | None = None
60+
locked_rev: str | None = None
61+
62+
63+
class NixRegistryEntry(BaseModel):
64+
from_name: str
65+
to_url: str
66+
67+
68+
class NixConfig(BaseModel):
69+
"""Key settings from nix.conf.
70+
71+
SECURITY: access-tokens and netrc-file values MUST be redacted before storing.
72+
"""
73+
74+
experimental_features: list[str] = []
75+
substituters: list[str] = []
76+
trusted_users: list[str] = []
77+
max_jobs: int | None = None
78+
sandbox: bool | None = None
79+
extra_config: dict[str, str] = Field(default_factory=dict)
80+
81+
82+
class DevboxProject(BaseModel):
83+
path: Path
84+
packages: list[str] = []
85+
86+
87+
class DevenvProject(BaseModel):
88+
path: Path
89+
has_lock: bool = False
90+
91+
92+
class NixDirenvConfig(BaseModel):
93+
"""Tracks .envrc files that use nix-direnv or use_nix."""
94+
95+
path: Path
96+
use_flake: bool = False
97+
use_nix: bool = False
98+
99+
100+
class NixState(BaseModel):
101+
"""Aggregate Nix ecosystem state."""
102+
103+
installation: NixInstallation = Field(default_factory=NixInstallation)
104+
profiles: list[NixProfile] = []
105+
darwin: NixDarwinState = Field(default_factory=NixDarwinState)
106+
home_manager: HomeManagerState = Field(default_factory=HomeManagerState)
107+
channels: list[NixChannel] = []
108+
flake_inputs: list[NixFlakeInput] = []
109+
registries: list[NixRegistryEntry] = []
110+
config: NixConfig = Field(default_factory=NixConfig)
111+
devbox_projects: list[DevboxProject] = []
112+
devenv_projects: list[DevenvProject] = []
113+
direnv_configs: list[NixDirenvConfig] = []
114+
115+
116+
class VersionManagerType(StrEnum):
117+
ASDF = "asdf"
118+
MISE = "mise"
119+
NVM = "nvm"
120+
PYENV = "pyenv"
121+
RBENV = "rbenv"
122+
JENV = "jenv"
123+
SDKMAN = "sdkman"
124+
125+
126+
class ManagedRuntime(BaseModel):
127+
"""A single runtime version managed by a version manager."""
128+
129+
manager: VersionManagerType
130+
language: str
131+
version: str
132+
path: Path | None = None
133+
active: bool = False
134+
135+
136+
class VersionManagerInfo(BaseModel):
137+
"""State of one version manager installation."""
138+
139+
manager_type: VersionManagerType
140+
version: str | None = None
141+
config_path: Path | None = None
142+
runtimes: list[ManagedRuntime] = []
143+
144+
145+
class VersionManagersResult(BaseModel):
146+
"""Aggregate version manager state."""
147+
148+
managers: list[VersionManagerInfo] = []
149+
global_tool_versions: Path | None = None
150+
151+
152+
class MacPortsPackage(BaseModel):
153+
name: str
154+
version: str | None = None
155+
active: bool = True
156+
variants: list[str] = []
157+
158+
159+
class MacPortsState(BaseModel):
160+
present: bool = False
161+
version: str | None = None
162+
prefix: Path = Path("/opt/local")
163+
packages: list[MacPortsPackage] = []
164+
165+
166+
class CondaPackage(BaseModel):
167+
name: str
168+
version: str | None = None
169+
channel: str | None = None
170+
171+
172+
class CondaEnvironment(BaseModel):
173+
name: str
174+
path: Path
175+
is_active: bool = False
176+
packages: list[CondaPackage] = []
177+
178+
179+
class CondaState(BaseModel):
180+
present: bool = False
181+
version: str | None = None
182+
environments: list[CondaEnvironment] = []
183+
184+
185+
class PackageManagersResult(BaseModel):
186+
"""Third-party (non-Homebrew, non-Nix) package managers."""
187+
188+
macports: MacPortsState = Field(default_factory=MacPortsState)
189+
conda: CondaState = Field(default_factory=CondaState)
190+
191+
192+
class ContainerRuntimeType(StrEnum):
193+
DOCKER = "docker"
194+
PODMAN = "podman"
195+
COLIMA = "colima"
196+
ORBSTACK = "orbstack"
197+
LIMA = "lima"
198+
199+
200+
class ContainerRuntimeInfo(BaseModel):
201+
runtime_type: ContainerRuntimeType
202+
version: str | None = None
203+
running: bool = False
204+
config_path: Path | None = None
205+
socket_path: Path | None = None
206+
207+
208+
class ContainersResult(BaseModel):
209+
runtimes: list[ContainerRuntimeInfo] = []

src/mac2nix/models/system.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from datetime import datetime
66
from typing import Any
77

8-
from pydantic import BaseModel
8+
from pydantic import BaseModel, Field
99

1010

1111
class NetworkInterface(BaseModel):
@@ -67,6 +67,23 @@ class PrinterInfo(BaseModel):
6767
options: dict[str, str] = {}
6868

6969

70+
class SystemExtension(BaseModel):
71+
"""A system extension from /Library/SystemExtensions/."""
72+
73+
identifier: str
74+
team_id: str | None = None
75+
version: str | None = None
76+
state: str | None = None
77+
78+
79+
class ICloudState(BaseModel):
80+
"""iCloud sync status — scan-only, cannot be configured via nix-darwin."""
81+
82+
signed_in: bool = False
83+
desktop_sync: bool = False
84+
documents_sync: bool = False
85+
86+
7087
class SystemConfig(BaseModel):
7188
hostname: str
7289
timezone: str | None = None
@@ -93,3 +110,7 @@ class SystemConfig(BaseModel):
93110
remote_login: bool | None = None
94111
screen_sharing: bool | None = None
95112
file_sharing: bool | None = None
113+
rosetta_installed: bool | None = None
114+
system_extensions: list[SystemExtension] = []
115+
icloud: ICloudState = Field(default_factory=ICloudState)
116+
mdm_enrolled: bool | None = None

0 commit comments

Comments
 (0)