net-install-manager 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- net_install_manager/__init__.py +17 -0
- net_install_manager/app_manager.py +339 -0
- net_install_manager/cli_output.py +23 -0
- net_install_manager/commands/__init__.py +0 -0
- net_install_manager/commands/app_details.py +59 -0
- net_install_manager/commands/install.py +114 -0
- net_install_manager/commands/install_release.py +142 -0
- net_install_manager/commands/list_apps.py +37 -0
- net_install_manager/commands/list_versions.py +68 -0
- net_install_manager/commands/rollback.py +59 -0
- net_install_manager/commands/uninstall.py +71 -0
- net_install_manager/commands/upgrade.py +272 -0
- net_install_manager/config.py +124 -0
- net_install_manager/errors.py +35 -0
- net_install_manager/net_install_manager.py +268 -0
- net_install_manager/paths.py +73 -0
- net_install_manager/registry.py +176 -0
- net_install_manager/runtime_config.py +59 -0
- net_install_manager/utilities/__init__.py +0 -0
- net_install_manager/utilities/csproj_tools.py +112 -0
- net_install_manager/utilities/git_tools.py +219 -0
- net_install_manager/utilities/github_release_tools.py +202 -0
- net_install_manager/utilities/install_tools.py +189 -0
- net_install_manager/utilities/os_ops.py +325 -0
- net_install_manager/utilities/resolve_manager.py +33 -0
- net_install_manager/utilities/sh.py +84 -0
- net_install_manager/utilities/sys_platform.py +133 -0
- net_install_manager/utilities/version_tools.py +117 -0
- net_install_manager-0.1.0.dist-info/METADATA +105 -0
- net_install_manager-0.1.0.dist-info/RECORD +33 -0
- net_install_manager-0.1.0.dist-info/WHEEL +4 -0
- net_install_manager-0.1.0.dist-info/entry_points.txt +3 -0
- net_install_manager-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Net Install Manager (`ninman`) package."""
|
|
2
|
+
|
|
3
|
+
from net_install_manager.config import Config, load_config, parse_config, auto_discover_config
|
|
4
|
+
from net_install_manager.paths import Paths, get_paths
|
|
5
|
+
from net_install_manager.app_manager import AppManager, AppManagerError, VersionRegressionError
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Config",
|
|
9
|
+
"load_config",
|
|
10
|
+
"parse_config",
|
|
11
|
+
"auto_discover_config",
|
|
12
|
+
"Paths",
|
|
13
|
+
"get_paths",
|
|
14
|
+
"AppManager",
|
|
15
|
+
"AppManagerError",
|
|
16
|
+
"VersionRegressionError",
|
|
17
|
+
]
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Unified application manager for installing and managing .NET applications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import logging
|
|
8
|
+
import semver
|
|
9
|
+
|
|
10
|
+
from net_install_manager.config import Config
|
|
11
|
+
from net_install_manager.paths import get_paths
|
|
12
|
+
from net_install_manager.registry import AppRegistry, TrackedApp
|
|
13
|
+
from net_install_manager.runtime_config import InstallOptions, RuntimeInfo
|
|
14
|
+
from net_install_manager.utilities.sys_platform import PlatformInfo, OperatingSystem
|
|
15
|
+
from net_install_manager.utilities.os_ops import (
|
|
16
|
+
OsOpsProtocol,
|
|
17
|
+
get_os_ops,
|
|
18
|
+
)
|
|
19
|
+
from net_install_manager.utilities.csproj_tools import (
|
|
20
|
+
publish as dotnet_publish,
|
|
21
|
+
)
|
|
22
|
+
from net_install_manager.utilities.version_tools import (
|
|
23
|
+
parse_semver,
|
|
24
|
+
resolve_version,
|
|
25
|
+
)
|
|
26
|
+
from net_install_manager.utilities.install_tools import (
|
|
27
|
+
InstallSource,
|
|
28
|
+
get_buildable_source,
|
|
29
|
+
get_paths_no_buildable_source,
|
|
30
|
+
get_paths_with_buildable_source,
|
|
31
|
+
)
|
|
32
|
+
from net_install_manager.errors import AppManagerError, VersionRegressionError
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AppManager:
|
|
38
|
+
"""Manages the installation, rollback, uninstallation, and listing of a .NET application."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
config: Config,
|
|
43
|
+
prefer_opt: bool = False,
|
|
44
|
+
runtime: RuntimeInfo | None = None,
|
|
45
|
+
os_ops: type[OsOpsProtocol] | None = None,
|
|
46
|
+
registry: AppRegistry | None = None,
|
|
47
|
+
):
|
|
48
|
+
self.config = config
|
|
49
|
+
self.runtime = runtime or RuntimeInfo.current()
|
|
50
|
+
self.os_ops = os_ops or get_os_ops(self.runtime.os)
|
|
51
|
+
self.registry = registry or AppRegistry(runtime=self.runtime)
|
|
52
|
+
self.paths = get_paths(
|
|
53
|
+
config=self.config,
|
|
54
|
+
prefer_opt=prefer_opt,
|
|
55
|
+
runtime=self.runtime,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def with_config(self, config: Config) -> AppManager:
|
|
59
|
+
"""Return a new AppManager instance with the given configuration."""
|
|
60
|
+
return AppManager(
|
|
61
|
+
config=config,
|
|
62
|
+
prefer_opt=self.prefer_opt,
|
|
63
|
+
runtime=self.runtime,
|
|
64
|
+
os_ops=self.os_ops,
|
|
65
|
+
registry=self.registry,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def prefer_opt(self) -> bool:
|
|
70
|
+
"""Return whether target paths use /opt."""
|
|
71
|
+
return self.paths.lib_dir.as_posix().startswith("/opt/")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def platform(self) -> PlatformInfo:
|
|
75
|
+
"""Return platform info for this application manager."""
|
|
76
|
+
return self.runtime.platform
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def target_os(self) -> OperatingSystem:
|
|
80
|
+
"""Return target operating system for backward compatibility."""
|
|
81
|
+
return self.runtime.os
|
|
82
|
+
|
|
83
|
+
def get_installed_versions(self) -> list[semver.Version]:
|
|
84
|
+
"""Return a sorted list of installed versions (highest first)."""
|
|
85
|
+
if not self.paths.versions_dir.exists() or not self.paths.versions_dir.is_dir():
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
versions: list[semver.Version] = []
|
|
89
|
+
for item in self.paths.versions_dir.iterdir():
|
|
90
|
+
if item.is_dir() and item.name != "current":
|
|
91
|
+
v = parse_semver(item.name)
|
|
92
|
+
if v is not None:
|
|
93
|
+
versions.append(v)
|
|
94
|
+
|
|
95
|
+
versions.sort(reverse=True)
|
|
96
|
+
return versions
|
|
97
|
+
|
|
98
|
+
def get_current_version(self) -> semver.Version | None:
|
|
99
|
+
"""Get the version currently targeted by the 'current' link/junction."""
|
|
100
|
+
if not self.paths.current_dir.exists(follow_symlinks=False):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
target = self.paths.current_dir.resolve()
|
|
105
|
+
return parse_semver(target.name)
|
|
106
|
+
except Exception: # pylint: disable=broad-except
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
def install(
|
|
110
|
+
self,
|
|
111
|
+
source: InstallSource,
|
|
112
|
+
options: InstallOptions,
|
|
113
|
+
registry_source: InstallSource | None = None,
|
|
114
|
+
) -> semver.Version:
|
|
115
|
+
"""Install application from source (.csproj, git repo, or precompiled directory)."""
|
|
116
|
+
buildable_source = get_buildable_source(source=source, quiet=options.quiet)
|
|
117
|
+
|
|
118
|
+
temp_staging_dir: Path | None = None
|
|
119
|
+
copy_source: Path
|
|
120
|
+
binary_path: Path
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
if buildable_source:
|
|
124
|
+
copy_source, binary_path, temp_staging_dir = get_paths_with_buildable_source(
|
|
125
|
+
binary_name=self.config.source_binary_name
|
|
126
|
+
)
|
|
127
|
+
logger.info(
|
|
128
|
+
"Publishing %s to %s...", buildable_source.csproj_path, temp_staging_dir
|
|
129
|
+
)
|
|
130
|
+
dotnet_publish(
|
|
131
|
+
csproj_path=buildable_source.csproj_path,
|
|
132
|
+
output_dir=temp_staging_dir,
|
|
133
|
+
opts=options.to_build_opts(platform=self.platform),
|
|
134
|
+
quiet=options.quiet,
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
copy_source, binary_path = get_paths_no_buildable_source(
|
|
138
|
+
binary_name=self.config.source_binary_name,
|
|
139
|
+
source=source,
|
|
140
|
+
quiet=options.quiet,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
version = resolve_version(
|
|
144
|
+
explicit_version=options.explicit_version,
|
|
145
|
+
csproj_path=buildable_source.csproj_path if buildable_source else None,
|
|
146
|
+
binary_path=binary_path,
|
|
147
|
+
source_dir=buildable_source.source_root if buildable_source else copy_source,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Version regression check
|
|
151
|
+
installed_versions = self.get_installed_versions()
|
|
152
|
+
if not options.force and installed_versions and installed_versions[0] >= version:
|
|
153
|
+
raise VersionRegressionError(
|
|
154
|
+
f"Version {installed_versions[0]} is already installed, which is newer than "
|
|
155
|
+
f"or equal to the version being installed ({version}). Use --force to override."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
target_version_dir = self.paths.versions_dir / str(version)
|
|
159
|
+
|
|
160
|
+
# Transfer files
|
|
161
|
+
logger.info("Transferring files to %s...", target_version_dir)
|
|
162
|
+
self.os_ops.mirror_directory(
|
|
163
|
+
src=copy_source,
|
|
164
|
+
dest=target_version_dir,
|
|
165
|
+
exclude_patterns=options.exclude_patterns,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Executable & Permissions
|
|
169
|
+
executable_path = target_version_dir / self.config.source_binary_name
|
|
170
|
+
self.os_ops.make_executable(executable_path)
|
|
171
|
+
if self.config.target_permissions:
|
|
172
|
+
self.os_ops.set_permissions(
|
|
173
|
+
target_version_dir,
|
|
174
|
+
permissions=self.config.target_permissions,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Update 'current' link
|
|
178
|
+
logger.info("Updating 'current' link...")
|
|
179
|
+
self.os_ops.update_current_link(
|
|
180
|
+
versions_dir=self.paths.versions_dir,
|
|
181
|
+
target_version=str(version),
|
|
182
|
+
current_dir=self.paths.current_dir,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Update launcher in bin
|
|
186
|
+
target_current_exec = self.paths.current_dir / self.config.source_binary_name
|
|
187
|
+
logger.info("Updating binary launcher in %s...", self.paths.bin_dir)
|
|
188
|
+
self.os_ops.create_bin_launcher(
|
|
189
|
+
bin_dir=self.paths.bin_dir,
|
|
190
|
+
bin_name=self.config.binary_name,
|
|
191
|
+
target_executable=target_current_exec,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Prune old versions if keep_latest specified
|
|
195
|
+
if options.keep_latest and options.keep_latest > 0:
|
|
196
|
+
self.prune_versions(options.keep_latest)
|
|
197
|
+
|
|
198
|
+
# Track in registry
|
|
199
|
+
try:
|
|
200
|
+
tracked = TrackedApp(
|
|
201
|
+
binary_name=self.config.binary_name,
|
|
202
|
+
app_directory=self.config.app_directory,
|
|
203
|
+
source_binary_name=self.config.source_binary_name,
|
|
204
|
+
source=registry_source
|
|
205
|
+
or (buildable_source.source if buildable_source else source),
|
|
206
|
+
lib_path=str(self.paths.lib_dir),
|
|
207
|
+
self_contained=options.self_contained,
|
|
208
|
+
release=options.release,
|
|
209
|
+
target_permissions=self.config.target_permissions,
|
|
210
|
+
installed_version=str(version),
|
|
211
|
+
)
|
|
212
|
+
self.registry.register(tracked)
|
|
213
|
+
except Exception as e: # pylint: disable=broad-except
|
|
214
|
+
logger.warning("Could not register app in registry: %s", e)
|
|
215
|
+
|
|
216
|
+
return version
|
|
217
|
+
|
|
218
|
+
finally:
|
|
219
|
+
if temp_staging_dir and temp_staging_dir.exists():
|
|
220
|
+
shutil.rmtree(temp_staging_dir, ignore_errors=True)
|
|
221
|
+
|
|
222
|
+
def prune_versions(self, keep_latest: int) -> list[semver.Version]:
|
|
223
|
+
"""Prune older installed versions, keeping only the specified number of latest versions."""
|
|
224
|
+
installed = self.get_installed_versions()
|
|
225
|
+
if len(installed) <= keep_latest:
|
|
226
|
+
return []
|
|
227
|
+
|
|
228
|
+
removed = []
|
|
229
|
+
for v in installed[keep_latest:]:
|
|
230
|
+
version_dir = self.paths.versions_dir / str(v)
|
|
231
|
+
if version_dir.exists():
|
|
232
|
+
self.os_ops.remove_link_or_dir(version_dir)
|
|
233
|
+
removed.append(v)
|
|
234
|
+
return removed
|
|
235
|
+
|
|
236
|
+
def rollback(
|
|
237
|
+
self, target_version: str | None = None, previous: bool = False
|
|
238
|
+
) -> semver.Version:
|
|
239
|
+
"""Rollback to a previous installed version."""
|
|
240
|
+
installed = self.get_installed_versions()
|
|
241
|
+
if not installed:
|
|
242
|
+
raise AppManagerError("No installed versions found to rollback to.")
|
|
243
|
+
|
|
244
|
+
current = self.get_current_version()
|
|
245
|
+
selected_version: semver.Version | None = None
|
|
246
|
+
|
|
247
|
+
if previous:
|
|
248
|
+
# Find the version directly before the current version
|
|
249
|
+
if current and len(installed) > 1:
|
|
250
|
+
for idx, v in enumerate(installed):
|
|
251
|
+
if v == current and idx + 1 < len(installed):
|
|
252
|
+
selected_version = installed[idx + 1]
|
|
253
|
+
break
|
|
254
|
+
if not selected_version and len(installed) > 1:
|
|
255
|
+
selected_version = installed[1]
|
|
256
|
+
elif not selected_version:
|
|
257
|
+
raise AppManagerError(
|
|
258
|
+
"Cannot rollback with --previous: only one version is installed."
|
|
259
|
+
)
|
|
260
|
+
elif target_version:
|
|
261
|
+
target_v = parse_semver(target_version)
|
|
262
|
+
if not target_v:
|
|
263
|
+
raise AppManagerError(f"Invalid semver version: '{target_version}'")
|
|
264
|
+
if target_v not in installed:
|
|
265
|
+
raise AppManagerError(f"Target version '{target_version}' is not installed.")
|
|
266
|
+
selected_version = target_v
|
|
267
|
+
else:
|
|
268
|
+
raise AppManagerError("Must specify either a target version or --previous.")
|
|
269
|
+
|
|
270
|
+
# Switch current link
|
|
271
|
+
self.os_ops.update_current_link(
|
|
272
|
+
versions_dir=self.paths.versions_dir,
|
|
273
|
+
target_version=str(selected_version),
|
|
274
|
+
current_dir=self.paths.current_dir,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Update binary launcher
|
|
278
|
+
target_current_exec = self.paths.current_dir / self.config.source_binary_name
|
|
279
|
+
self.os_ops.create_bin_launcher(
|
|
280
|
+
bin_dir=self.paths.bin_dir,
|
|
281
|
+
bin_name=self.config.binary_name,
|
|
282
|
+
target_executable=target_current_exec,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return selected_version
|
|
286
|
+
|
|
287
|
+
def uninstall(self, target_version: str | None = None, all_versions: bool = False) -> None:
|
|
288
|
+
"""Uninstall a specific version or the entire application."""
|
|
289
|
+
if all_versions:
|
|
290
|
+
# Remove versions dir and current
|
|
291
|
+
if self.target_os == OperatingSystem.WINDOWS:
|
|
292
|
+
app_root = self.paths.lib_dir.parent
|
|
293
|
+
self.os_ops.remove_link_or_dir(app_root)
|
|
294
|
+
else:
|
|
295
|
+
self.os_ops.remove_link_or_dir(self.paths.lib_dir)
|
|
296
|
+
|
|
297
|
+
# Remove launchers
|
|
298
|
+
cmd_launcher = self.paths.bin_dir / f"{self.config.binary_name}.cmd"
|
|
299
|
+
posix_launcher = self.paths.bin_dir / self.config.binary_name
|
|
300
|
+
self.os_ops.remove_link_or_dir(cmd_launcher)
|
|
301
|
+
self.os_ops.remove_link_or_dir(posix_launcher)
|
|
302
|
+
|
|
303
|
+
# Remove from tracking registry
|
|
304
|
+
try:
|
|
305
|
+
self.registry.remove(self.config.binary_name)
|
|
306
|
+
except Exception as e: # pylint: disable=broad-except
|
|
307
|
+
logger.warning("Could not remove app from registry: %s", e)
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
if target_version:
|
|
311
|
+
target_v = parse_semver(target_version)
|
|
312
|
+
if not target_v:
|
|
313
|
+
raise AppManagerError(f"Invalid semver version: '{target_version}'")
|
|
314
|
+
|
|
315
|
+
target_dir = self.paths.versions_dir / str(target_v)
|
|
316
|
+
if not target_dir.exists():
|
|
317
|
+
raise AppManagerError(f"Version '{target_version}' is not installed.")
|
|
318
|
+
|
|
319
|
+
current = self.get_current_version()
|
|
320
|
+
|
|
321
|
+
self.os_ops.remove_link_or_dir(target_dir)
|
|
322
|
+
|
|
323
|
+
# If the current link pointed to the deleted version, fallback to newest remaining
|
|
324
|
+
if current == target_v:
|
|
325
|
+
remaining = self.get_installed_versions()
|
|
326
|
+
if remaining:
|
|
327
|
+
self.rollback(target_version=str(remaining[0]))
|
|
328
|
+
else:
|
|
329
|
+
self.os_ops.remove_link_or_dir(self.paths.current_dir)
|
|
330
|
+
cmd_launcher = self.paths.bin_dir / f"{self.config.binary_name}.cmd"
|
|
331
|
+
posix_launcher = self.paths.bin_dir / self.config.binary_name
|
|
332
|
+
self.os_ops.remove_link_or_dir(cmd_launcher)
|
|
333
|
+
self.os_ops.remove_link_or_dir(posix_launcher)
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
raise AppManagerError("Must specify either --target-version or --all for uninstall.")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
__all__ = ["AppManager", "AppManagerError", "VersionRegressionError"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""User-facing CLI output helpers."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import TextIO
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def write(message: str, stream: TextIO | None = None) -> None:
|
|
8
|
+
"""Write a user-facing line to the selected stream."""
|
|
9
|
+
print(message, file=stream or sys.stdout)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def error(message: str) -> None:
|
|
13
|
+
"""Write a user-facing error line to stderr."""
|
|
14
|
+
write(message, stream=sys.stderr)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def progress(message: str, quiet: bool = False) -> None:
|
|
18
|
+
"""Write progress output unless quiet mode is enabled."""
|
|
19
|
+
if not quiet:
|
|
20
|
+
write(message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ["error", "progress", "write"]
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Retrieve and display details about a .NET application."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from net_install_manager.runtime_config import RuntimeInfo
|
|
6
|
+
from net_install_manager.registry import AppRegistry
|
|
7
|
+
from net_install_manager.cli_output import write, error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(
|
|
11
|
+
args: argparse.Namespace,
|
|
12
|
+
registry: AppRegistry | None = None,
|
|
13
|
+
runtime: RuntimeInfo | None = None,
|
|
14
|
+
) -> int:
|
|
15
|
+
"""Retrieve and display details about a ninman-managed application."""
|
|
16
|
+
runtime = runtime or RuntimeInfo.current(system_wide=getattr(args, "system", False))
|
|
17
|
+
registry = registry or AppRegistry(runtime=runtime)
|
|
18
|
+
app_name = args.app_name
|
|
19
|
+
|
|
20
|
+
details = registry.get(app_name)
|
|
21
|
+
if not details:
|
|
22
|
+
error(f"No details found for application '{app_name}'.")
|
|
23
|
+
return 1
|
|
24
|
+
|
|
25
|
+
write(f"Details for application '{app_name}':")
|
|
26
|
+
|
|
27
|
+
write(f" Lib path: {details.lib_path}")
|
|
28
|
+
write(f" Version: {details.installed_version}")
|
|
29
|
+
write(f" Binary: {details.binary_name}")
|
|
30
|
+
write(f" Source binary: {details.source_binary_name}")
|
|
31
|
+
|
|
32
|
+
write(" Source:")
|
|
33
|
+
if details.source.git_info:
|
|
34
|
+
write(" Source type: Git Repository Source")
|
|
35
|
+
write(f" Clone URL: {details.source.git_info.clone_url}")
|
|
36
|
+
write(f" Subpath: {details.source.git_info.subpath}")
|
|
37
|
+
write(f" Ref: {details.source.git_info.ref}")
|
|
38
|
+
elif details.source.release_info:
|
|
39
|
+
write(" Source type: GitHub Release Artifact")
|
|
40
|
+
write(f" Owner: {details.source.release_info.owner}")
|
|
41
|
+
write(f" Repo: {details.source.release_info.repo}")
|
|
42
|
+
write(f" Asset pattern: {details.source.release_info.asset_pattern}")
|
|
43
|
+
write(f" Tag: {details.source.release_info.tag}")
|
|
44
|
+
elif details.source.local_path:
|
|
45
|
+
write(" Source type: Local Path")
|
|
46
|
+
write(f" Path: {details.source.local_path}")
|
|
47
|
+
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def register_app_details_command(
|
|
52
|
+
subparsers: argparse._SubParsersAction,
|
|
53
|
+
) -> argparse.ArgumentParser:
|
|
54
|
+
"""Register the command to display application details."""
|
|
55
|
+
parser = subparsers.add_parser(
|
|
56
|
+
"app-details", help="Display details about a ninman-managed application"
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument("app_name", help="Name of the ninman-managed application")
|
|
59
|
+
return parser
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Install command for the Net Install Manager application."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from net_install_manager.app_manager import AppManager, VersionRegressionError
|
|
7
|
+
from net_install_manager.cli_output import error, write
|
|
8
|
+
from net_install_manager.runtime_config import InstallOptions
|
|
9
|
+
from net_install_manager.utilities.git_tools import is_git_url, parse_git_source
|
|
10
|
+
from net_install_manager.utilities.install_tools import InstallSource
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_options(args: argparse.Namespace) -> InstallOptions:
|
|
14
|
+
"""Parse InstallOptions from command-line arguments."""
|
|
15
|
+
return InstallOptions(
|
|
16
|
+
self_contained=getattr(args, "self_contained", False),
|
|
17
|
+
release=not getattr(args, "debug", False),
|
|
18
|
+
keep_latest=getattr(args, "versions_to_keep", None),
|
|
19
|
+
force=getattr(args, "force", False),
|
|
20
|
+
explicit_version=getattr(args, "app_version", None),
|
|
21
|
+
quiet=getattr(args, "quiet", False),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_source(args: argparse.Namespace, manager: AppManager) -> InstallSource:
|
|
26
|
+
"""Parse the install source and project selector from CLI arguments."""
|
|
27
|
+
source = args.source
|
|
28
|
+
project_path = getattr(args, "project", None)
|
|
29
|
+
if source and is_git_url(str(source)):
|
|
30
|
+
git_info = parse_git_source(str(source), project_override=project_path)
|
|
31
|
+
return InstallSource(
|
|
32
|
+
git_info=git_info,
|
|
33
|
+
project_path=git_info.subpath,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
source_path = source or manager.config.csproj_path or Path.cwd()
|
|
37
|
+
return InstallSource(local_path=str(source_path), project_path=project_path)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def execute(args: argparse.Namespace, manager: AppManager):
|
|
41
|
+
"""Execute the install command."""
|
|
42
|
+
if args.versions_to_keep is not None and args.versions_to_keep < 1:
|
|
43
|
+
error("The number of versions to keep must be at least 1.")
|
|
44
|
+
return 1
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
options = parse_options(args)
|
|
48
|
+
installed_version = manager.install(
|
|
49
|
+
source=parse_source(args, manager),
|
|
50
|
+
options=options,
|
|
51
|
+
)
|
|
52
|
+
write(
|
|
53
|
+
f"Successfully installed version {installed_version} of '{manager.config.binary_name}'."
|
|
54
|
+
)
|
|
55
|
+
return 0
|
|
56
|
+
except VersionRegressionError as e:
|
|
57
|
+
error(f"Error: {e}")
|
|
58
|
+
return 6
|
|
59
|
+
except Exception as e: # pylint: disable=broad-except
|
|
60
|
+
error(f"Installation failed: {e}")
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def register_install_command(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
65
|
+
"""Register the install command arguments."""
|
|
66
|
+
parser = subparsers.add_parser("install", help="Install a .NET application")
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"source",
|
|
69
|
+
nargs="?",
|
|
70
|
+
default=None,
|
|
71
|
+
help="Path to .csproj file, project folder, precompiled directory, or Git/GitHub URL",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"-p",
|
|
75
|
+
"--project",
|
|
76
|
+
"--project-path",
|
|
77
|
+
dest="project",
|
|
78
|
+
default=None,
|
|
79
|
+
help="Path to the target .csproj file within the repository or directory",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--self-contained",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="Install as self-contained application (includes .NET runtime)",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--debug",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="Build using Debug configuration instead of Release",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"-k",
|
|
93
|
+
"--versions-to-keep",
|
|
94
|
+
type=int,
|
|
95
|
+
default=None,
|
|
96
|
+
help="Number of previous versions to keep",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"-f",
|
|
100
|
+
"--force",
|
|
101
|
+
action="store_true",
|
|
102
|
+
help="Force reinstallation even if version is already installed or older",
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"-v",
|
|
106
|
+
"--app-version",
|
|
107
|
+
type=str,
|
|
108
|
+
default=None,
|
|
109
|
+
help="Explicit version string (overrides automatic detection)",
|
|
110
|
+
)
|
|
111
|
+
return parser
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
__all__ = ["execute", "register_install_command"]
|