pkgmgr-kunrunic 0.1.1.dev0__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.
Potentially problematic release.
This version of pkgmgr-kunrunic might be problematic. Click here for more details.
- pkgmgr/__init__.py +17 -0
- pkgmgr/__main__.py +5 -0
- pkgmgr/cli.py +278 -0
- pkgmgr/collectors/__init__.py +5 -0
- pkgmgr/collectors/base.py +15 -0
- pkgmgr/collectors/checksums.py +35 -0
- pkgmgr/config.py +380 -0
- pkgmgr/gitcollect.py +10 -0
- pkgmgr/points.py +98 -0
- pkgmgr/release.py +579 -0
- pkgmgr/shell_integration.py +83 -0
- pkgmgr/snapshot.py +306 -0
- pkgmgr/templates/pkg.yaml.sample +16 -0
- pkgmgr/templates/pkgmgr.yaml.sample +51 -0
- pkgmgr/watch.py +79 -0
- pkgmgr_kunrunic-0.1.1.dev0.dist-info/METADATA +147 -0
- pkgmgr_kunrunic-0.1.1.dev0.dist-info/RECORD +21 -0
- pkgmgr_kunrunic-0.1.1.dev0.dist-info/WHEEL +5 -0
- pkgmgr_kunrunic-0.1.1.dev0.dist-info/entry_points.txt +2 -0
- pkgmgr_kunrunic-0.1.1.dev0.dist-info/licenses/LICENSE +21 -0
- pkgmgr_kunrunic-0.1.1.dev0.dist-info/top_level.txt +1 -0
pkgmgr/snapshot.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from __future__ import print_function
|
|
2
|
+
"""Snapshot utilities: hashing and state persistence."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import fnmatch
|
|
8
|
+
import time
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from . import config
|
|
12
|
+
|
|
13
|
+
STATE_DIR = config.DEFAULT_STATE_DIR
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DuplicateBaselineError(RuntimeError):
|
|
17
|
+
"""Raised when attempting to create a baseline that already exists."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProgressReporter(object):
|
|
21
|
+
"""TTY-friendly one-line progress reporter (no-op when stdout is not a TTY)."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, prefix):
|
|
24
|
+
self.prefix = prefix
|
|
25
|
+
self._is_tty = sys.stdout.isatty()
|
|
26
|
+
self._last_len = 0
|
|
27
|
+
self._label = None
|
|
28
|
+
self._total = 0
|
|
29
|
+
self._current = 0
|
|
30
|
+
|
|
31
|
+
def start(self, label, total):
|
|
32
|
+
if not self._is_tty:
|
|
33
|
+
return
|
|
34
|
+
self._label = label
|
|
35
|
+
self._total = int(total or 0)
|
|
36
|
+
self._current = 0
|
|
37
|
+
self._render()
|
|
38
|
+
|
|
39
|
+
def advance(self, step=1):
|
|
40
|
+
if not self._is_tty:
|
|
41
|
+
return
|
|
42
|
+
self._current += int(step or 0)
|
|
43
|
+
if self._current > self._total:
|
|
44
|
+
self._current = self._total
|
|
45
|
+
self._render()
|
|
46
|
+
|
|
47
|
+
def finish(self):
|
|
48
|
+
if not self._is_tty:
|
|
49
|
+
return
|
|
50
|
+
if self._total == 0:
|
|
51
|
+
self._current = 0
|
|
52
|
+
else:
|
|
53
|
+
self._current = self._total
|
|
54
|
+
self._render(final=True)
|
|
55
|
+
sys.stdout.write("\n")
|
|
56
|
+
sys.stdout.flush()
|
|
57
|
+
|
|
58
|
+
def _render(self, final=False):
|
|
59
|
+
total = self._total
|
|
60
|
+
current = self._current
|
|
61
|
+
denom = total if total > 0 else 1
|
|
62
|
+
pct = int((float(current) / float(denom)) * 100)
|
|
63
|
+
if total == 0 and final:
|
|
64
|
+
pct = 100
|
|
65
|
+
bar_len = 30
|
|
66
|
+
filled = int((float(current) / float(denom)) * bar_len)
|
|
67
|
+
bar = "#" * filled + "-" * (bar_len - filled)
|
|
68
|
+
label = self._label or ""
|
|
69
|
+
line = "[%s] %s %d/%d %3d%% [%s]" % (
|
|
70
|
+
self.prefix,
|
|
71
|
+
label,
|
|
72
|
+
current,
|
|
73
|
+
total,
|
|
74
|
+
pct,
|
|
75
|
+
bar,
|
|
76
|
+
)
|
|
77
|
+
pad = " " * max(0, self._last_len - len(line))
|
|
78
|
+
sys.stdout.write("\r" + line + pad)
|
|
79
|
+
sys.stdout.flush()
|
|
80
|
+
self._last_len = len(line)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _ensure_state_dir():
|
|
84
|
+
if not os.path.exists(STATE_DIR):
|
|
85
|
+
os.makedirs(STATE_DIR)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _sha256(path, chunk=1024 * 1024):
|
|
89
|
+
h = hashlib.sha256()
|
|
90
|
+
f = open(path, "rb")
|
|
91
|
+
try:
|
|
92
|
+
while True:
|
|
93
|
+
b = f.read(chunk)
|
|
94
|
+
if not b:
|
|
95
|
+
break
|
|
96
|
+
h.update(b)
|
|
97
|
+
finally:
|
|
98
|
+
f.close()
|
|
99
|
+
return h.hexdigest()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _should_skip(relpath, patterns):
|
|
103
|
+
for p in patterns or []:
|
|
104
|
+
if fnmatch.fnmatch(relpath, p):
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _count_files(root_abs, exclude):
|
|
110
|
+
total = 0
|
|
111
|
+
for base, _, files in os.walk(root_abs):
|
|
112
|
+
for name in files:
|
|
113
|
+
abspath = os.path.join(base, name)
|
|
114
|
+
rel = os.path.relpath(abspath, root_abs).replace("\\", "/")
|
|
115
|
+
if _should_skip(rel, exclude):
|
|
116
|
+
continue
|
|
117
|
+
total += 1
|
|
118
|
+
return total
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _scan(root, exclude, progress=None, label=None):
|
|
122
|
+
res = {}
|
|
123
|
+
root_abs = os.path.abspath(os.path.expanduser(root))
|
|
124
|
+
if not os.path.exists(root_abs):
|
|
125
|
+
print("[snap] skip missing root: %s" % root_abs)
|
|
126
|
+
return res
|
|
127
|
+
if progress and label:
|
|
128
|
+
total = _count_files(root_abs, exclude)
|
|
129
|
+
progress.start(label, total)
|
|
130
|
+
for base, _, files in os.walk(root_abs):
|
|
131
|
+
for name in files:
|
|
132
|
+
abspath = os.path.join(base, name)
|
|
133
|
+
rel = os.path.relpath(abspath, root_abs).replace("\\", "/")
|
|
134
|
+
if _should_skip(rel, exclude):
|
|
135
|
+
continue
|
|
136
|
+
try:
|
|
137
|
+
st = os.stat(abspath)
|
|
138
|
+
res[rel] = {
|
|
139
|
+
"hash": _sha256(abspath),
|
|
140
|
+
"size": int(st.st_size),
|
|
141
|
+
"mtime": int(st.st_mtime),
|
|
142
|
+
}
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print("[snap] warn skip %s: %s" % (abspath, str(e)))
|
|
145
|
+
if progress:
|
|
146
|
+
progress.advance()
|
|
147
|
+
if progress and label:
|
|
148
|
+
progress.finish()
|
|
149
|
+
return res
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _maybe_keep_existing_baseline(path, prompt_overwrite):
|
|
153
|
+
"""
|
|
154
|
+
When prompt_overwrite=True and baseline exists, ask user (if tty) whether to overwrite.
|
|
155
|
+
Raises DuplicateBaselineError when overwrite is declined or non-interactive.
|
|
156
|
+
"""
|
|
157
|
+
if not prompt_overwrite:
|
|
158
|
+
return None
|
|
159
|
+
if not os.path.exists(path):
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if not sys.stdin.isatty():
|
|
163
|
+
msg = "[baseline] existing baseline at %s; non-tty -> refusing overwrite" % path
|
|
164
|
+
raise DuplicateBaselineError(msg)
|
|
165
|
+
|
|
166
|
+
ans = input("[baseline] existing baseline at %s; overwrite? [y/N]: " % path).strip().lower()
|
|
167
|
+
if ans not in ("y", "yes"):
|
|
168
|
+
msg = "[baseline] keeping existing baseline; skipped overwrite"
|
|
169
|
+
raise DuplicateBaselineError(msg)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _scan_artifacts(cfg, progress=None):
|
|
174
|
+
"""
|
|
175
|
+
Scan artifact roots/targets similar to sources.
|
|
176
|
+
artifacts.root: base path (optional)
|
|
177
|
+
artifacts.targets: names or absolute paths
|
|
178
|
+
artifacts.exclude: patterns
|
|
179
|
+
"""
|
|
180
|
+
artifacts_cfg = cfg.get("artifacts") or {}
|
|
181
|
+
art_root = artifacts_cfg.get("root")
|
|
182
|
+
art_targets = artifacts_cfg.get("targets") or []
|
|
183
|
+
art_exclude = artifacts_cfg.get("exclude") or []
|
|
184
|
+
|
|
185
|
+
result = {}
|
|
186
|
+
base_root = os.path.abspath(os.path.expanduser(art_root)) if art_root else None
|
|
187
|
+
for t in art_targets:
|
|
188
|
+
target_str = str(t)
|
|
189
|
+
if base_root and not os.path.isabs(target_str):
|
|
190
|
+
target_path = os.path.join(base_root, target_str)
|
|
191
|
+
else:
|
|
192
|
+
target_path = target_str
|
|
193
|
+
target_path = os.path.abspath(os.path.expanduser(target_path))
|
|
194
|
+
label = "artifact %s" % target_path
|
|
195
|
+
result[target_path] = _scan(target_path, art_exclude, progress=progress, label=label)
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def create_baseline(cfg, prompt_overwrite=False, progress=None):
|
|
200
|
+
"""
|
|
201
|
+
Collect initial baseline snapshot.
|
|
202
|
+
Scans sources and artifacts (if configured).
|
|
203
|
+
"""
|
|
204
|
+
_ensure_state_dir()
|
|
205
|
+
sources = cfg.get("sources", []) or []
|
|
206
|
+
src_exclude = (cfg.get("source") or {}).get("exclude", []) or []
|
|
207
|
+
|
|
208
|
+
snapshot_data = {
|
|
209
|
+
"meta": {
|
|
210
|
+
"ts": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()),
|
|
211
|
+
"type": "baseline",
|
|
212
|
+
},
|
|
213
|
+
"sources": {},
|
|
214
|
+
"artifacts": {},
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for root in sources:
|
|
218
|
+
label = "source %s" % root
|
|
219
|
+
snapshot_data["sources"][root] = _scan(
|
|
220
|
+
root, src_exclude, progress=progress, label=label
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
snapshot_data["artifacts"] = _scan_artifacts(cfg, progress=progress)
|
|
224
|
+
|
|
225
|
+
path = os.path.join(STATE_DIR, "baseline.json")
|
|
226
|
+
existing = _maybe_keep_existing_baseline(path, prompt_overwrite)
|
|
227
|
+
if existing is not None:
|
|
228
|
+
return existing
|
|
229
|
+
|
|
230
|
+
f = open(path, "w")
|
|
231
|
+
try:
|
|
232
|
+
json.dump(snapshot_data, f, ensure_ascii=False, indent=2, sort_keys=True)
|
|
233
|
+
finally:
|
|
234
|
+
f.close()
|
|
235
|
+
print("[baseline] saved to %s" % path)
|
|
236
|
+
return snapshot_data
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def create_snapshot(cfg, progress=None):
|
|
240
|
+
"""
|
|
241
|
+
Collect a fresh snapshot (for updates).
|
|
242
|
+
"""
|
|
243
|
+
_ensure_state_dir()
|
|
244
|
+
sources = cfg.get("sources", []) or []
|
|
245
|
+
src_exclude = (cfg.get("source") or {}).get("exclude", []) or []
|
|
246
|
+
|
|
247
|
+
snapshot_data = {
|
|
248
|
+
"meta": {
|
|
249
|
+
"ts": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime()),
|
|
250
|
+
"type": "snapshot",
|
|
251
|
+
},
|
|
252
|
+
"sources": {},
|
|
253
|
+
"artifacts": {},
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for root in sources:
|
|
257
|
+
label = "source %s" % root
|
|
258
|
+
snapshot_data["sources"][root] = _scan(
|
|
259
|
+
root, src_exclude, progress=progress, label=label
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
snapshot_data["artifacts"] = _scan_artifacts(cfg, progress=progress)
|
|
263
|
+
|
|
264
|
+
path = os.path.join(STATE_DIR, "snapshot.json")
|
|
265
|
+
f = open(path, "w")
|
|
266
|
+
try:
|
|
267
|
+
json.dump(snapshot_data, f, ensure_ascii=False, indent=2, sort_keys=True)
|
|
268
|
+
finally:
|
|
269
|
+
f.close()
|
|
270
|
+
print("[snap] snapshot saved to %s" % path)
|
|
271
|
+
return snapshot_data
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def diff_snapshots(base, latest):
|
|
275
|
+
"""Diff two snapshot dicts."""
|
|
276
|
+
added = []
|
|
277
|
+
modified = []
|
|
278
|
+
deleted = []
|
|
279
|
+
|
|
280
|
+
def _diff_map(a, b):
|
|
281
|
+
a_keys = set(a.keys())
|
|
282
|
+
b_keys = set(b.keys())
|
|
283
|
+
for k in b_keys - a_keys:
|
|
284
|
+
added.append(k)
|
|
285
|
+
for k in a_keys - b_keys:
|
|
286
|
+
deleted.append(k)
|
|
287
|
+
for k in a_keys & b_keys:
|
|
288
|
+
if a[k].get("hash") != b[k].get("hash"):
|
|
289
|
+
modified.append(k)
|
|
290
|
+
|
|
291
|
+
# flatten per-root
|
|
292
|
+
def _flatten_section(snap, section):
|
|
293
|
+
flat = {}
|
|
294
|
+
for root, entries in (snap or {}).get(section, {}).items():
|
|
295
|
+
for rel, meta in (entries or {}).items():
|
|
296
|
+
flat[root + "/" + rel] = meta
|
|
297
|
+
return flat
|
|
298
|
+
|
|
299
|
+
a_flat = {}
|
|
300
|
+
b_flat = {}
|
|
301
|
+
for section in ("sources", "artifacts"):
|
|
302
|
+
a_flat.update(_flatten_section(base or {}, section))
|
|
303
|
+
b_flat.update(_flatten_section(latest or {}, section))
|
|
304
|
+
_diff_map(a_flat, b_flat)
|
|
305
|
+
|
|
306
|
+
return {"added": sorted(added), "modified": sorted(modified), "deleted": sorted(deleted)}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
pkg:
|
|
2
|
+
id: "<pkg-id>"
|
|
3
|
+
root: "/path/to/release/<pkg-id>"
|
|
4
|
+
status: "open"
|
|
5
|
+
|
|
6
|
+
include:
|
|
7
|
+
releases: []
|
|
8
|
+
|
|
9
|
+
git:
|
|
10
|
+
repo_root: null # optional override; default is git rev-parse from cwd
|
|
11
|
+
keywords: []
|
|
12
|
+
since: null
|
|
13
|
+
until: null
|
|
14
|
+
|
|
15
|
+
collectors:
|
|
16
|
+
enabled: ["checksums"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
pkg_release_root: ~/PKG/RELEASE
|
|
2
|
+
sources:
|
|
3
|
+
- /path/to/source-A
|
|
4
|
+
- /path/to/source-B
|
|
5
|
+
|
|
6
|
+
source:
|
|
7
|
+
# 소스 스캔 제외 (glob 지원)
|
|
8
|
+
exclude:
|
|
9
|
+
- "**/build/**"
|
|
10
|
+
- "**/*.tmp"
|
|
11
|
+
- "**/bk"
|
|
12
|
+
- "**/*.sc*"
|
|
13
|
+
- "**/unit_test/**"
|
|
14
|
+
- "Jamrules*"
|
|
15
|
+
- "Jamfile*"
|
|
16
|
+
- "**/Jamrules*"
|
|
17
|
+
- "**/Jamfile*"
|
|
18
|
+
|
|
19
|
+
artifacts:
|
|
20
|
+
root: ~/HOME
|
|
21
|
+
targets: [bin, lib, data]
|
|
22
|
+
# 배포/설치 영역 제외 (glob/패턴)
|
|
23
|
+
exclude:
|
|
24
|
+
- log # 정확히 log 디렉터리
|
|
25
|
+
- tmp/** # tmp 이하 전체
|
|
26
|
+
- "*.bak" # 확장자 패턴
|
|
27
|
+
- "**/*.tmp" # 모든 하위에서 .tmp
|
|
28
|
+
|
|
29
|
+
watch:
|
|
30
|
+
interval_sec: 60
|
|
31
|
+
on_change: [] # 변경 발생 시 실행할 action 이름 리스트
|
|
32
|
+
|
|
33
|
+
collectors:
|
|
34
|
+
enabled: ["checksums"]
|
|
35
|
+
|
|
36
|
+
actions:
|
|
37
|
+
# 각 액션은 하나 이상의 커맨드를 가질 수 있음.
|
|
38
|
+
# 필드: cmd (필수, 보통 cwd 기준 경로), cwd(선택), env(선택; 이 커맨드에만 적용)
|
|
39
|
+
export_cksum:
|
|
40
|
+
- cmd: python cksum_excel.py # cwd 기준 실행
|
|
41
|
+
cwd: /app/script # 선택: 작업 디렉터리
|
|
42
|
+
env: { APP_ENV: dev } # 선택: 이 명령에만 적용할 환경변수
|
|
43
|
+
export_world_dev:
|
|
44
|
+
- cmd: python dev_world.py
|
|
45
|
+
cwd: /app/script
|
|
46
|
+
export_world_security:
|
|
47
|
+
- cmd: python security_world.py
|
|
48
|
+
cwd: /app/script
|
|
49
|
+
noti_email:
|
|
50
|
+
- cmd: sh noti_email.sh
|
|
51
|
+
cwd: /app/script
|
pkgmgr/watch.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import print_function
|
|
2
|
+
"""Watcher/daemon scaffold."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from . import snapshot, release, points
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(cfg, run_once=False, pkg_id=None, auto_point=False, point_label=None):
|
|
12
|
+
"""
|
|
13
|
+
Basic poller:
|
|
14
|
+
- loads last point snapshot (if pkg_id provided) or baseline
|
|
15
|
+
- takes new snapshot
|
|
16
|
+
- if diff exists, run watch.on_change actions
|
|
17
|
+
- optionally create a point after actions
|
|
18
|
+
"""
|
|
19
|
+
interval = cfg.get("watch", {}).get("interval_sec", 60)
|
|
20
|
+
print("[watch] starting poller interval=%ss once=%s pkg=%s auto_point=%s" % (interval, run_once, pkg_id, auto_point))
|
|
21
|
+
if run_once:
|
|
22
|
+
_tick(cfg, pkg_id=pkg_id, auto_point=auto_point, point_label=point_label)
|
|
23
|
+
return
|
|
24
|
+
while True:
|
|
25
|
+
_tick(cfg, pkg_id=pkg_id, auto_point=auto_point, point_label=point_label)
|
|
26
|
+
time.sleep(interval)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _load_json(path):
|
|
30
|
+
try:
|
|
31
|
+
with open(path, "r") as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
except Exception:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _previous_snapshot(pkg_id):
|
|
38
|
+
"""Return previous snapshot data for diff: latest point snapshot if available, else baseline."""
|
|
39
|
+
if pkg_id:
|
|
40
|
+
_, snap = points.load_latest_point(pkg_id)
|
|
41
|
+
if snap:
|
|
42
|
+
return snap
|
|
43
|
+
baseline_path = os.path.join(snapshot.STATE_DIR, "baseline.json")
|
|
44
|
+
return _load_json(baseline_path)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _tick(cfg, pkg_id=None, auto_point=False, point_label=None):
|
|
48
|
+
if pkg_id and release.pkg_is_closed(pkg_id):
|
|
49
|
+
print("[watch] pkg=%s is closed; skipping poll" % pkg_id)
|
|
50
|
+
return
|
|
51
|
+
prev_snap = _previous_snapshot(pkg_id)
|
|
52
|
+
current_snap = snapshot.create_snapshot(cfg)
|
|
53
|
+
if prev_snap:
|
|
54
|
+
diff = snapshot.diff_snapshots(prev_snap, current_snap)
|
|
55
|
+
if not any(diff.values()):
|
|
56
|
+
print("[watch] no changes since last point/baseline")
|
|
57
|
+
return
|
|
58
|
+
print("[watch] changes detected: added=%d modified=%d deleted=%d" % (len(diff["added"]), len(diff["modified"]), len(diff["deleted"])))
|
|
59
|
+
else:
|
|
60
|
+
print("[watch] no previous snapshot; treating as initial run")
|
|
61
|
+
diff = None
|
|
62
|
+
|
|
63
|
+
actions_to_run = (cfg.get("watch") or {}).get("on_change", []) or []
|
|
64
|
+
results = []
|
|
65
|
+
if actions_to_run:
|
|
66
|
+
results = release.run_actions(cfg, actions_to_run)
|
|
67
|
+
else:
|
|
68
|
+
print("[watch] no watch.on_change actions configured")
|
|
69
|
+
|
|
70
|
+
if auto_point and pkg_id:
|
|
71
|
+
label = point_label or "watch-auto"
|
|
72
|
+
release.create_point(
|
|
73
|
+
cfg,
|
|
74
|
+
pkg_id,
|
|
75
|
+
label=label,
|
|
76
|
+
actions_run=actions_to_run,
|
|
77
|
+
actions_result=results,
|
|
78
|
+
snapshot_data=current_snap,
|
|
79
|
+
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pkgmgr-kunrunic
|
|
3
|
+
Version: 0.1.1.dev0
|
|
4
|
+
Summary: Package management workflow scaffold.
|
|
5
|
+
Author: pkgmgr maintainers
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2025 Jho Sung Jun
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/kunrunic/pkgmgnt
|
|
29
|
+
Project-URL: Repository, https://github.com/kunrunic/pkgmgnt
|
|
30
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Programming Language :: Python
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
44
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
45
|
+
Requires-Python: >=3.6
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
License-File: LICENSE
|
|
48
|
+
Requires-Dist: pyyaml>=6.0
|
|
49
|
+
Dynamic: license-file
|
|
50
|
+
|
|
51
|
+
# pkgmgr
|
|
52
|
+
|
|
53
|
+
패키지 관리/배포 워크플로를 위한 Python 패키지입니다. 현재는 스캐폴드 상태이며, CLI와 설정 템플릿, 릴리스 번들링까지 제공됩니다.
|
|
54
|
+
|
|
55
|
+
## 디자인/Use case
|
|
56
|
+
- 흐름 요약과 Mermaid 시퀀스 다이어그램은 [`design/mermaid/use-cases.md`](design/mermaid/use-cases.md)에 있습니다.
|
|
57
|
+
- 주 명령별 설정/동작 요약과 make-config/install/create-pkg/update-pkg/close-pkg 시퀀스를 포함합니다.
|
|
58
|
+
|
|
59
|
+
## 구성
|
|
60
|
+
- `pkgmgr/cli.py` : CLI 엔트리 (아래 명령어 참조)
|
|
61
|
+
- `pkgmgr/config.py` : `pkgmgr.yaml` / `pkg.yaml` 템플릿 생성 및 로더 (PyYAML 필요)
|
|
62
|
+
- `pkgmgr/snapshot.py`, `pkgmgr/release.py`, `pkgmgr/gitcollect.py`, `pkgmgr/watch.py` : 스냅샷/패키지 수명주기/깃 수집/감시/릴리스 번들
|
|
63
|
+
- `pkgmgr/collectors/` : 컬렉터 인터페이스 및 체크섬 컬렉터 스텁
|
|
64
|
+
- 템플릿: `pkgmgr/templates/pkgmgr.yaml.sample`, `pkgmgr/templates/pkg.yaml.sample`
|
|
65
|
+
|
|
66
|
+
## 필요 사항
|
|
67
|
+
- Python 3.6 이상
|
|
68
|
+
- 의존성(PyYAML 등)은 `pip install pkgmgr-kunrunic` 시 자동 설치됩니다.
|
|
69
|
+
|
|
70
|
+
## 설치 (PyPI/로컬)
|
|
71
|
+
- PyPI(권장): `python -m pip install pkgmgr-kunrunic` (또는 `pipx install pkgmgr-kunrunic`으로 전역 CLI 설치).
|
|
72
|
+
- 로컬/개발: 리포지토리 클론 후 `python -m pip install .` 또는 빌드 산출물(`dist/pkgmgr_kunrunic-<버전>-py3-none-any.whl`)을 `python -m pip install dist/<파일>`로 설치.
|
|
73
|
+
- 확인: `pkgmgr --version` 혹은 `python -m pkgmgr.cli --version`.
|
|
74
|
+
|
|
75
|
+
## 기본 사용 흐름 (스캐폴드)
|
|
76
|
+
아래 명령은 `pkgmgr ...` 또는 `python -m pkgmgr.cli ...` 형태로 실행합니다.
|
|
77
|
+
설정 파일 기본 위치는 `~/pkgmgr/pkgmgr.yaml`이며, `~/pkgmgr/pkgmgr*.yaml`과 `~/pkgmgr/config/pkgmgr*.yaml`을 자동 탐색합니다(여러 개면 선택 필요, `--config`로 강제 지정 가능). 상태/릴리스 데이터는 `~/pkgmgr/local/state` 아래에 기록됩니다.
|
|
78
|
+
|
|
79
|
+
### 1) make-config — 메인 설정 템플릿 생성
|
|
80
|
+
```
|
|
81
|
+
pkgmgr make-config
|
|
82
|
+
pkgmgr make-config -o ~/pkgmgr/config/pkgmgr-alt.yaml # 위치 지정 가능
|
|
83
|
+
```
|
|
84
|
+
편집할 주요 필드: `pkg_release_root`, `sources`, `source.exclude`, `artifacts.targets/exclude`, `git.keywords/repo_root`, `collectors.enabled`, `actions`.
|
|
85
|
+
|
|
86
|
+
### 2) install — PATH/alias 등록 + 초기 baseline(한 번만)
|
|
87
|
+
```
|
|
88
|
+
pkgmgr install [--config <path>]
|
|
89
|
+
```
|
|
90
|
+
- 사용 쉘을 감지해 rc 파일에 PATH/alias 추가.
|
|
91
|
+
- `~/pkgmgr/local/state/baseline.json`이 없을 때만 초기 스냅샷 생성(있으면 건너뜀).
|
|
92
|
+
|
|
93
|
+
### 3) create-pkg — 패키지 디렉터리/설정 생성
|
|
94
|
+
```
|
|
95
|
+
pkgmgr create-pkg <pkg-id> [--config <path>]
|
|
96
|
+
```
|
|
97
|
+
- `<pkg_release_root>/<pkg-id>/pkg.yaml`을 실제 값으로 채워 생성(기존 파일이 있으면 덮어쓰기 여부 확인).
|
|
98
|
+
- 메인 설정의 `git.keywords/repo_root`, `collectors.enabled`를 기본값으로 반영.
|
|
99
|
+
- baseline이 없는 경우에만 baseline 생성.
|
|
100
|
+
|
|
101
|
+
### 4) update-pkg — Git/체크섬 수집 + 릴리스 번들 생성
|
|
102
|
+
```
|
|
103
|
+
pkgmgr update-pkg <pkg-id> [--config <path>]
|
|
104
|
+
```
|
|
105
|
+
- Git: `git.repo_root`(상대/절대)에서 `git.keywords` 매칭 커밋을 모아 `message/author/subject/files/keywords` 저장.
|
|
106
|
+
- 체크섬: 키워드에 걸린 파일 + `include.releases` 경로의 파일 해시 수집.
|
|
107
|
+
- 릴리스 번들: `include.releases` 최상위 디렉터리별로 `release/<root>/release.vX.Y.Z/`를 자동 증가 버전으로 생성. 이전 버전과 해시가 동일한 파일은 스킵, 달라진 파일만 복사. 각 버전 폴더에 README.txt(포함/스킵 수, tar 예시, TODO) 작성. tar는 만들지 않음.
|
|
108
|
+
- 실행 결과는 `~/pkgmgr/local/state/pkg/<id>/updates/update-<ts>.json`에 기록(`git`, `checksums`, `release` 메타 포함).
|
|
109
|
+
|
|
110
|
+
### (기타) watch / collect / actions / export / point
|
|
111
|
+
기능은 여전히 스텁 혹은 최소 구현 상태입니다. 필요 시 `pkgmgr watch`, `pkgmgr collect`, `pkgmgr actions`, `pkgmgr point`, `pkgmgr export`를 이용할 수 있으며, 추후 강화 예정입니다.
|
|
112
|
+
|
|
113
|
+
## PATH/alias 자동 추가
|
|
114
|
+
- PyPI/로컬 설치 후 `python -m pkgmgr.cli install`을 실행하면 현재 파이썬의 `bin` 경로(예: venv/bin, ~/.local/bin 등)를 감지해 사용 중인 쉘의 rc 파일에 PATH/alias를 추가합니다.
|
|
115
|
+
- 지원 쉘: bash(`~/.bashrc`), zsh(`~/.zshrc`), csh/tcsh(`~/.cshrc`/`~/.tcshrc`), fish(`~/.config/fish/config.fish`).
|
|
116
|
+
- 추가 내용:
|
|
117
|
+
- PATH: `export PATH="<script_dir>:$PATH"` 또는 쉘별 동등 구문
|
|
118
|
+
- alias: `alias pkg="pkgmgr"` (csh/fish 문법 사용)
|
|
119
|
+
- 이미 추가된 경우(marker로 확인) 중복 삽입하지 않습니다. rc 파일이 없으면 새로 만듭니다.
|
|
120
|
+
|
|
121
|
+
## 템플릿 개요
|
|
122
|
+
- `pkgmgr/templates/pkgmgr.yaml.sample` : 메인 설정 샘플
|
|
123
|
+
- `pkg_release_root`: 패키지 릴리스 루트
|
|
124
|
+
- `sources`: 관리할 소스 경로 목록
|
|
125
|
+
- `source.exclude`: 소스 스캔 제외 패턴 (glob 지원)
|
|
126
|
+
- `artifacts.targets` / `artifacts.exclude`: 배포 대상 포함/제외 규칙 (glob 지원: `tmp/**`, `*.bak`, `**/*.tmp` 등)
|
|
127
|
+
- `watch.interval_sec`: 감시 폴링 주기
|
|
128
|
+
- `watch.on_change`: 변경 시 실행할 action 이름 리스트
|
|
129
|
+
- `collectors.enabled`: 기본 활성 컬렉터
|
|
130
|
+
- `actions`: action 이름 → 실행할 커맨드 목록 (각 항목에 `cmd` 필수, `cwd`/`env` 선택)
|
|
131
|
+
|
|
132
|
+
- `pkgmgr/templates/pkg.yaml.sample` : 패키지별 설정 샘플
|
|
133
|
+
- `pkg.id` / `pkg.root` / `pkg.status(open|closed)`
|
|
134
|
+
- `include.releases`: 릴리스에 포함할 경로(최상위 디렉터리별로 묶여 `release/<root>/release.vX.Y.Z` 생성)
|
|
135
|
+
- `git.repo_root/keywords/since/until`: 커밋 수집 범위
|
|
136
|
+
- `collectors.enabled`: 패키지별 컬렉터 설정
|
|
137
|
+
|
|
138
|
+
## 주의
|
|
139
|
+
- 아직 핵심 로직(스냅샷/감시/수집/내보내기)은 스텁입니다. 추후 단계적으로 구현/교체 예정입니다.
|
|
140
|
+
|
|
141
|
+
## TODO (우선순위)
|
|
142
|
+
- 감시/포인트 고도화: watchdog/inotify 연동, diff 결과를 포인트 메타에 기록, 에러/로그 처리.
|
|
143
|
+
- baseline/릴리스 알림: baseline 대비 변경 감지 시 알림/확인 흐름 추가(README/README.txt TODO 반영).
|
|
144
|
+
- Git 수집: `gitcollect.py` 확장(키워드/정규식 수집, state/pkg/<id>/commits.json 저장).
|
|
145
|
+
- 컬렉터 파이프라인: 체크섬 외 collector 등록/선택/실행 로직, include 기준 실행, 정적/동적/EDR/AV 훅 자리 마련.
|
|
146
|
+
- Export/산출물: `export` 기본 JSON 덤프, excel/word 후크 설계, 포인트 단위 산출물 묶기.
|
|
147
|
+
- 테스트/CI: watch diff/포인트/라이프사이클 단위 테스트 추가, pytest/CI 스크립트 보강.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
pkgmgr/__init__.py,sha256=RwVr63xHsTjN7fh7bxc07JRwpa_f8F-HV7gVX1syOHU,288
|
|
2
|
+
pkgmgr/__main__.py,sha256=5eNtvDmnY8k4F1RUzYaPcmF-6t-4kH8THzm8x8vFZGI,117
|
|
3
|
+
pkgmgr/cli.py,sha256=CJcuSYQoifX3NEFHGjLvrdpNQJ6gAe5lKd27s_XBlVw,8065
|
|
4
|
+
pkgmgr/config.py,sha256=aq44DXzXKsc5H2S96VosnVDNwsK9kCSfxp7bcCXVyyk,12506
|
|
5
|
+
pkgmgr/gitcollect.py,sha256=MiPSUsEVO81bbc4g5dzlQajyszLBxUu3yQwHwLp8exI,340
|
|
6
|
+
pkgmgr/points.py,sha256=6RxcqJYzMLSmLdvoRmmfsrT5O2uDMrJtGHPTYxJE9Xw,3177
|
|
7
|
+
pkgmgr/release.py,sha256=gWnkFSgWz6fji-GBtDEv6I2U0Tsq7fhS6bzklSBmyd4,19553
|
|
8
|
+
pkgmgr/shell_integration.py,sha256=jH3uN2ReNjXTXyQqWlO8UUvBw9nrj8SRP3-QZVhGxHk,2597
|
|
9
|
+
pkgmgr/snapshot.py,sha256=STHhqf_nW51JnOisVeT58gPEr2SawNbq-V3xlt9ZXEE,9030
|
|
10
|
+
pkgmgr/watch.py,sha256=7FeKdSOVNZZnIb12KwSs90NLkUAAn4iM130rNE7Z8oc,2642
|
|
11
|
+
pkgmgr/collectors/__init__.py,sha256=eDt5wa8jyDFn3HC2-0U5AjyXSdlJ9hcHOweMewfD4kM,90
|
|
12
|
+
pkgmgr/collectors/base.py,sha256=Zfpe6yMMLmoSZQUkYR6TObDF4TW1YQNNlAZmcukTQIU,309
|
|
13
|
+
pkgmgr/collectors/checksums.py,sha256=Lp7ySNx6ZEhk6hMWtMgJ5KJn0PcWa6jb9_FXosd6Dks,877
|
|
14
|
+
pkgmgr/templates/pkg.yaml.sample,sha256=9nm9Jj-98y9AjrAirx1AF3XAvFjkuQFI0n5fIpJWUSk,261
|
|
15
|
+
pkgmgr/templates/pkgmgr.yaml.sample,sha256=-0QXDwPbFr52G9-mxjQBbMOEZz7JrJ0ZVTUcvzytu2A,1384
|
|
16
|
+
pkgmgr_kunrunic-0.1.1.dev0.dist-info/licenses/LICENSE,sha256=DQyjlSl_S-KOdFowc6Lqx-cP6cX9aNF9CLguAHfQI5c,1069
|
|
17
|
+
pkgmgr_kunrunic-0.1.1.dev0.dist-info/METADATA,sha256=APue8TyHsrLavY0YB34Cc0GpQBbo8ccsw2Xfk6ZB9vk,8909
|
|
18
|
+
pkgmgr_kunrunic-0.1.1.dev0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
pkgmgr_kunrunic-0.1.1.dev0.dist-info/entry_points.txt,sha256=DsYKXAiVGhesLkCOM0jgsRoiSXo21S8qReKGTqxTBjs,43
|
|
20
|
+
pkgmgr_kunrunic-0.1.1.dev0.dist-info/top_level.txt,sha256=eQM_xxe8mhHcwmMik-aja8Z_IB7SpUJ9ow3iPlXlBhw,7
|
|
21
|
+
pkgmgr_kunrunic-0.1.1.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jho Sung Jun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pkgmgr
|