pkgmgr-kunrunic 0.1.1.dev4__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.
- pkgmgr/__init__.py +16 -0
- pkgmgr/__main__.py +5 -0
- pkgmgr/cli.py +320 -0
- pkgmgr/collectors/__init__.py +5 -0
- pkgmgr/collectors/base.py +15 -0
- pkgmgr/collectors/checksums.py +35 -0
- pkgmgr/config.py +408 -0
- pkgmgr/points.py +98 -0
- pkgmgr/release.py +1031 -0
- pkgmgr/shell_integration.py +120 -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.dev4.dist-info/METADATA +159 -0
- pkgmgr_kunrunic-0.1.1.dev4.dist-info/RECORD +24 -0
- pkgmgr_kunrunic-0.1.1.dev4.dist-info/WHEEL +5 -0
- pkgmgr_kunrunic-0.1.1.dev4.dist-info/entry_points.txt +2 -0
- pkgmgr_kunrunic-0.1.1.dev4.dist-info/licenses/LICENSE +21 -0
- pkgmgr_kunrunic-0.1.1.dev4.dist-info/top_level.txt +3 -0
- plugin/export_cksum.py +354 -0
- plugin/export_pkgstore.py +117 -0
- plugin/export_source_review.py +499 -0
- tools/echo_args.py +15 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pkgmgr-kunrunic
|
|
3
|
+
Version: 0.1.1.dev4
|
|
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 패키지입니다. 현재는 패키지 단위 관리와 릴리스 번들링에 초점을 둔 초기 버전입니다.
|
|
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/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
|
+
- 최신 릴리스 종료/아카이브: `pkgmgr update-pkg <pkg-id> --release` (tar 생성 후 `HISTORY/`로 이동).
|
|
106
|
+
- Git: `git.repo_root`(상대/절대)에서 `git.keywords` 매칭 커밋을 모아 `message/author/subject/files/keywords` 저장.
|
|
107
|
+
- 체크섬: 키워드에 걸린 파일 + `include.releases` 경로의 파일 해시 수집.
|
|
108
|
+
- 릴리스 번들: `include.releases` 최상위 디렉터리별로 `release/<root>/release.vX.Y.Z/`를 생성. `--release` 전까지는 최신 버전을 유지하며 변경분만 추가/덮어쓰기/삭제 반영(버전 증가 없음), 이전 버전과 해시가 동일한 파일은 스킵. 각 릴리스 폴더에 `PKG_NOTE`(1회 생성, 사용자 내용 유지)와 `PKG_LIST`(매번 갱신) 작성.
|
|
109
|
+
- 실행 결과는 `~/pkgmgr/local/state/pkg/<id>/updates/update-<ts>.json`에 기록(`git`, `checksums`, `release` 메타 포함).
|
|
110
|
+
|
|
111
|
+
### 5) actions — 외부 작업 실행
|
|
112
|
+
```
|
|
113
|
+
pkgmgr actions
|
|
114
|
+
pkgmgr --config <path> actions <name> [args...]
|
|
115
|
+
```
|
|
116
|
+
- 설정의 `actions`에 등록된 작업 목록을 출력하거나, 지정한 작업을 실행합니다.
|
|
117
|
+
- `<name>` 뒤의 모든 인자는 액션 커맨드에 그대로 전달됩니다.
|
|
118
|
+
- 예: `pkgmgr --config ~/pkgmgr/pkgmgr.yaml actions export_cksum --root R --time 4`
|
|
119
|
+
- 예: `pkgmgr --config ~/pkgmgr/pkgmgr.yaml actions export_cksum --pkg-dir /path/to/pkg --excel /path/to/template.xlsx`
|
|
120
|
+
|
|
121
|
+
## PATH/alias 자동 추가
|
|
122
|
+
- PyPI/로컬 설치 후 `python -m pkgmgr.cli install`을 실행하면 현재 파이썬의 `bin` 경로(예: venv/bin, ~/.local/bin 등)를 감지해 사용 중인 쉘의 rc 파일에 PATH/alias를 추가합니다.
|
|
123
|
+
- 지원 쉘: bash(`~/.bashrc`), zsh(`~/.zshrc`), csh/tcsh(`~/.cshrc`/`~/.tcshrc`), fish(`~/.config/fish/config.fish`).
|
|
124
|
+
- 추가 내용:
|
|
125
|
+
- PATH: `export PATH="<script_dir>:$PATH"` 또는 쉘별 동등 구문
|
|
126
|
+
- alias: `alias pkg="pkgmgr"` (csh/fish 문법 사용)
|
|
127
|
+
- 이미 추가된 경우(marker로 확인) 중복 삽입하지 않습니다. rc 파일이 없으면 새로 만듭니다.
|
|
128
|
+
|
|
129
|
+
## 템플릿 개요
|
|
130
|
+
- `pkgmgr/templates/pkgmgr.yaml.sample` : 메인 설정 샘플
|
|
131
|
+
- `pkg_release_root`: 패키지 릴리스 루트
|
|
132
|
+
- `sources`: 관리할 소스 경로 목록
|
|
133
|
+
- `source.exclude`: 소스 스캔 제외 패턴 (glob 지원)
|
|
134
|
+
- `artifacts.targets` / `artifacts.exclude`: 배포 대상 포함/제외 규칙 (glob 지원: `tmp/**`, `*.bak`, `**/*.tmp` 등)
|
|
135
|
+
- `watch.interval_sec`: 감시 폴링 주기(향후 노출 예정)
|
|
136
|
+
- `watch.on_change`: 변경 시 실행할 action 이름 리스트(향후 노출 예정)
|
|
137
|
+
- `collectors.enabled`: 기본 활성 컬렉터(향후 확장 예정)
|
|
138
|
+
- `actions`: action 이름 → 실행할 커맨드 목록 (각 항목에 `cmd` 필수, `cwd`/`env` 선택)
|
|
139
|
+
|
|
140
|
+
- `pkgmgr/templates/pkg.yaml.sample` : 패키지별 설정 샘플
|
|
141
|
+
- `pkg.id` / `pkg.root` / `pkg.status(open|closed)`
|
|
142
|
+
- `include.releases`: 릴리스에 포함할 경로(최상위 디렉터리별로 묶여 `release/<root>/release.vX.Y.Z` 생성)
|
|
143
|
+
- `git.repo_root/keywords/since/until`: 커밋 수집 범위
|
|
144
|
+
- `collectors.enabled`: 패키지별 컬렉터 설정
|
|
145
|
+
|
|
146
|
+
## 주의
|
|
147
|
+
- 시스템 전체 관리(감시/수집/포인트) 기능은 아직 확장 단계입니다. 추후 단계적으로 구현/교체 예정입니다.
|
|
148
|
+
|
|
149
|
+
## 확장성 가이드
|
|
150
|
+
- `actions`를 기본 확장 포인트로 사용합니다. 배포/내보내기/알림 등은 액션으로 위임하는 것을 권장합니다.
|
|
151
|
+
- 릴리스 번들 포맷(`release/<root>/release.vX.Y.Z/`, `PKG_LIST`, `PKG_NOTE`)은 외부 도구와의 연동 기준점으로 사용합니다.
|
|
152
|
+
- `~/pkgmgr/local/state/pkg/<id>/updates/update-<ts>.json`은 자동화 파이프라인에서 읽을 수 있는 결과물로 취급합니다.
|
|
153
|
+
- 전역 수집/집계는 `collectors` 확장으로 흡수할 계획이며, CLI로 노출하기 전까지는 내부 확장용으로 유지합니다.
|
|
154
|
+
|
|
155
|
+
## TODO (우선순위)
|
|
156
|
+
- 감시/포인트 고도화: watchdog/inotify 연동, diff 결과를 포인트 메타에 기록, 에러/로그 처리.
|
|
157
|
+
- baseline/릴리스 알림: baseline 대비 변경 감지 시 알림/확인 흐름 추가(README/README.txt TODO 반영).
|
|
158
|
+
- 컬렉터 파이프라인: 체크섬 외 collector 등록/선택/실행 로직, include 기준 실행, 정적/동적/EDR/AV 훅 자리 마련.
|
|
159
|
+
- 테스트/CI: watch diff/포인트/라이프사이클 단위 테스트 추가, pytest/CI 스크립트 보강.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
pkgmgr/__init__.py,sha256=YXso8YOYaKROJdRdSjJzOEr2gv4esU4GZN6rNjNhbv8,270
|
|
2
|
+
pkgmgr/__main__.py,sha256=5eNtvDmnY8k4F1RUzYaPcmF-6t-4kH8THzm8x8vFZGI,117
|
|
3
|
+
pkgmgr/cli.py,sha256=ynyn_Mv8CNYuxJSefAAncFAz-qUUF2wwx8kRuVj0A3M,9590
|
|
4
|
+
pkgmgr/config.py,sha256=EXaA9wLDJr3wiOj46vE9LJKZcURI1UPr_9U3BPjymbM,13722
|
|
5
|
+
pkgmgr/points.py,sha256=6RxcqJYzMLSmLdvoRmmfsrT5O2uDMrJtGHPTYxJE9Xw,3177
|
|
6
|
+
pkgmgr/release.py,sha256=1614yD7xQC0uN0TyJJdebsKit-qUNA0cEom5OK0EEDw,35729
|
|
7
|
+
pkgmgr/shell_integration.py,sha256=79MT1Y1ZAN020JDMN3J0mcEgsmC3OlGT6W_f8ArLEUU,3856
|
|
8
|
+
pkgmgr/snapshot.py,sha256=STHhqf_nW51JnOisVeT58gPEr2SawNbq-V3xlt9ZXEE,9030
|
|
9
|
+
pkgmgr/watch.py,sha256=7FeKdSOVNZZnIb12KwSs90NLkUAAn4iM130rNE7Z8oc,2642
|
|
10
|
+
pkgmgr/collectors/__init__.py,sha256=eDt5wa8jyDFn3HC2-0U5AjyXSdlJ9hcHOweMewfD4kM,90
|
|
11
|
+
pkgmgr/collectors/base.py,sha256=Zfpe6yMMLmoSZQUkYR6TObDF4TW1YQNNlAZmcukTQIU,309
|
|
12
|
+
pkgmgr/collectors/checksums.py,sha256=Lp7ySNx6ZEhk6hMWtMgJ5KJn0PcWa6jb9_FXosd6Dks,877
|
|
13
|
+
pkgmgr/templates/pkg.yaml.sample,sha256=9nm9Jj-98y9AjrAirx1AF3XAvFjkuQFI0n5fIpJWUSk,261
|
|
14
|
+
pkgmgr/templates/pkgmgr.yaml.sample,sha256=9l39yqJkZTG960Tx5pm4e7p9oS3vLFdy65wOg_v-slg,1417
|
|
15
|
+
pkgmgr_kunrunic-0.1.1.dev4.dist-info/licenses/LICENSE,sha256=DQyjlSl_S-KOdFowc6Lqx-cP6cX9aNF9CLguAHfQI5c,1069
|
|
16
|
+
plugin/export_cksum.py,sha256=laYDz215mmbKhZzRtTV7p0LBU3RoWtnN5ygsU3MSnws,13624
|
|
17
|
+
plugin/export_pkgstore.py,sha256=zhzZuce_q7cwSul9tcIJlCaGZp3zlRFY2s4sQk3vglA,4431
|
|
18
|
+
plugin/export_source_review.py,sha256=5KXn5w8Cc15tBfNqPI65YVAu-SMK4bmA05WO4dVE8KA,16257
|
|
19
|
+
tools/echo_args.py,sha256=Uh_11LzHvI5xYbWR_xC_7cUphEeyOWEBXgnKYXqgwHM,219
|
|
20
|
+
pkgmgr_kunrunic-0.1.1.dev4.dist-info/METADATA,sha256=NxROILwIpW8aSG9A4U_eF9G2As82XRAg-SHl2WJeVMM,9748
|
|
21
|
+
pkgmgr_kunrunic-0.1.1.dev4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
pkgmgr_kunrunic-0.1.1.dev4.dist-info/entry_points.txt,sha256=DsYKXAiVGhesLkCOM0jgsRoiSXo21S8qReKGTqxTBjs,43
|
|
23
|
+
pkgmgr_kunrunic-0.1.1.dev4.dist-info/top_level.txt,sha256=emuhQN2YYcS6sgkErR4Xz-F1xvP8UaZ4FnBUH0LVM_k,20
|
|
24
|
+
pkgmgr_kunrunic-0.1.1.dev4.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.
|
plugin/export_cksum.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from copy import copy
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import openpyxl
|
|
11
|
+
except Exception:
|
|
12
|
+
openpyxl = None
|
|
13
|
+
|
|
14
|
+
from pkgmgr import config
|
|
15
|
+
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_pkg_yaml(pkg_dir):
|
|
19
|
+
if not pkg_dir:
|
|
20
|
+
return None
|
|
21
|
+
return os.path.join(pkg_dir, "pkg.yaml")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _resolve_pkg_dir(pkg_id, config_path=None):
|
|
25
|
+
try:
|
|
26
|
+
if config_path:
|
|
27
|
+
main_cfg = config.load_main(path=config_path, allow_interactive=False)
|
|
28
|
+
else:
|
|
29
|
+
main_cfg = config.load_main(allow_interactive=False)
|
|
30
|
+
except Exception:
|
|
31
|
+
return None
|
|
32
|
+
release_root = main_cfg.get("pkg_release_root")
|
|
33
|
+
if not release_root:
|
|
34
|
+
return None
|
|
35
|
+
return os.path.abspath(os.path.expanduser(os.path.join(release_root, str(pkg_id))))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _group_release_paths(pkg_dir, releases):
|
|
39
|
+
grouped = {}
|
|
40
|
+
for entry in releases:
|
|
41
|
+
if entry is None:
|
|
42
|
+
continue
|
|
43
|
+
rel = str(entry)
|
|
44
|
+
target = rel if os.path.isabs(rel) else os.path.join(pkg_dir, rel)
|
|
45
|
+
target = os.path.abspath(os.path.expanduser(target))
|
|
46
|
+
if not os.path.exists(target):
|
|
47
|
+
print("[export_cksum] skip missing: %s" % target)
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
relpath = os.path.relpath(target, pkg_dir)
|
|
51
|
+
except Exception:
|
|
52
|
+
relpath = os.path.basename(target)
|
|
53
|
+
if relpath.startswith(".."):
|
|
54
|
+
print("[export_cksum] skip outside pkg_dir: %s" % target)
|
|
55
|
+
continue
|
|
56
|
+
parts = relpath.split(os.sep, 1)
|
|
57
|
+
if len(parts) == 2:
|
|
58
|
+
root, subrel = parts[0], parts[1]
|
|
59
|
+
else:
|
|
60
|
+
root, subrel = parts[0], ""
|
|
61
|
+
grouped.setdefault(root, []).append((target, subrel))
|
|
62
|
+
return grouped
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _collect_files(root_dir, entries):
|
|
66
|
+
files = set()
|
|
67
|
+
for target, subrel in entries:
|
|
68
|
+
if subrel:
|
|
69
|
+
base_dir = os.path.join(root_dir, subrel)
|
|
70
|
+
else:
|
|
71
|
+
base_dir = target
|
|
72
|
+
if os.path.isfile(base_dir):
|
|
73
|
+
files.add(base_dir)
|
|
74
|
+
continue
|
|
75
|
+
if not os.path.isdir(base_dir):
|
|
76
|
+
continue
|
|
77
|
+
for base, _, names in os.walk(base_dir):
|
|
78
|
+
for name in names:
|
|
79
|
+
abspath = os.path.join(base, name)
|
|
80
|
+
if os.path.isfile(abspath):
|
|
81
|
+
files.add(abspath)
|
|
82
|
+
return files
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _cksum(path):
|
|
86
|
+
try:
|
|
87
|
+
out = subprocess.check_output(["cksum", path], stderr=subprocess.STDOUT)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print("[export_cksum] cksum failed: %s (%s)" % (path, str(e)))
|
|
90
|
+
return None
|
|
91
|
+
line = out.decode("utf-8", errors="replace").strip()
|
|
92
|
+
parts = line.split()
|
|
93
|
+
if len(parts) < 3:
|
|
94
|
+
print("[export_cksum] invalid cksum output: %s" % line)
|
|
95
|
+
return None
|
|
96
|
+
return parts[0], parts[1], " ".join(parts[2:])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _default_styles():
|
|
100
|
+
base_font = Font(name="맑은 고딕", size=11)
|
|
101
|
+
bold_font = Font(name="맑은 고딕", size=11, bold=True)
|
|
102
|
+
header_fill = PatternFill("solid", fgColor="92D050")
|
|
103
|
+
sub_fill = PatternFill("solid", fgColor="FCE4D6")
|
|
104
|
+
medium = Side(style="medium")
|
|
105
|
+
hair = Side(style="hair")
|
|
106
|
+
return {
|
|
107
|
+
"base_font": base_font,
|
|
108
|
+
"bold_font": bold_font,
|
|
109
|
+
"header_fill": header_fill,
|
|
110
|
+
"sub_fill": sub_fill,
|
|
111
|
+
"medium": medium,
|
|
112
|
+
"hair": hair,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _border(left=None, right=None, top=None, bottom=None):
|
|
117
|
+
return Border(left=left, right=right, top=top, bottom=bottom)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _init_sheet(ws, sheet_name, release_path, ensure_format):
|
|
121
|
+
if ensure_format:
|
|
122
|
+
styles = _default_styles()
|
|
123
|
+
ws.merge_cells("B2:E2")
|
|
124
|
+
ws.merge_cells("B3:E3")
|
|
125
|
+
if "B4:C4" not in ws.merged_cells:
|
|
126
|
+
ws.merge_cells("B4:C4")
|
|
127
|
+
for col in range(2, 6):
|
|
128
|
+
cell = ws.cell(row=2, column=col)
|
|
129
|
+
cell.fill = styles["header_fill"]
|
|
130
|
+
cell.font = styles["bold_font"]
|
|
131
|
+
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
132
|
+
cell.border = _border(styles["medium"], styles["medium"], styles["medium"], styles["medium"])
|
|
133
|
+
for col in range(2, 6):
|
|
134
|
+
cell = ws.cell(row=3, column=col)
|
|
135
|
+
cell.fill = styles["sub_fill"]
|
|
136
|
+
cell.font = styles["base_font"]
|
|
137
|
+
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
138
|
+
left = styles["medium"] if col == 2 else styles["hair"]
|
|
139
|
+
right = styles["medium"] if col == 5 else styles["hair"]
|
|
140
|
+
cell.border = _border(left, right, styles["medium"], styles["hair"])
|
|
141
|
+
for col in range(2, 6):
|
|
142
|
+
cell = ws.cell(row=4, column=col)
|
|
143
|
+
cell.fill = styles["sub_fill"]
|
|
144
|
+
cell.font = styles["base_font"]
|
|
145
|
+
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
146
|
+
left = styles["medium"] if col == 2 else styles["hair"]
|
|
147
|
+
right = styles["medium"] if col == 5 else styles["hair"]
|
|
148
|
+
cell.border = _border(left, right, styles["hair"], styles["hair"])
|
|
149
|
+
ws.row_dimensions[2].height = 17.25
|
|
150
|
+
ws.column_dimensions["B"].width = 16.75
|
|
151
|
+
ws.column_dimensions["C"].width = 12.75
|
|
152
|
+
ws.column_dimensions["D"].width = 45.875
|
|
153
|
+
ws.column_dimensions["E"].width = 12.75
|
|
154
|
+
ws.column_dimensions["F"].width = 13.0
|
|
155
|
+
ws.cell(row=2, column=2, value=sheet_name)
|
|
156
|
+
ws.cell(row=3, column=2, value=release_path)
|
|
157
|
+
ws.cell(row=4, column=2, value="Check Sum")
|
|
158
|
+
ws.cell(row=4, column=4, value="File Name")
|
|
159
|
+
ws.cell(row=4, column=5, value="비고")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _apply_table_border(ws, start_row, end_row, ensure_format):
|
|
163
|
+
if not ensure_format:
|
|
164
|
+
return
|
|
165
|
+
styles = _default_styles()
|
|
166
|
+
for row in range(start_row, end_row + 1):
|
|
167
|
+
for col in range(2, 6):
|
|
168
|
+
cell = ws.cell(row=row, column=col)
|
|
169
|
+
left = styles["medium"] if col == 2 else styles["hair"]
|
|
170
|
+
right = styles["medium"] if col == 5 else styles["hair"]
|
|
171
|
+
top = styles["hair"] if row >= 4 else None
|
|
172
|
+
if row == end_row:
|
|
173
|
+
bottom = styles["medium"]
|
|
174
|
+
elif row == 4:
|
|
175
|
+
bottom = styles["hair"]
|
|
176
|
+
else:
|
|
177
|
+
bottom = None
|
|
178
|
+
cell.border = _border(left, right, top, bottom)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _configure_page(ws, end_row, template_ws=None):
|
|
182
|
+
if template_ws is not None:
|
|
183
|
+
ws.print_area = template_ws.print_area
|
|
184
|
+
ws.page_setup.orientation = template_ws.page_setup.orientation
|
|
185
|
+
ws.page_setup.paperSize = template_ws.page_setup.paperSize
|
|
186
|
+
ws.page_setup.fitToWidth = template_ws.page_setup.fitToWidth
|
|
187
|
+
ws.page_setup.fitToHeight = template_ws.page_setup.fitToHeight
|
|
188
|
+
ws.sheet_view.view = template_ws.sheet_view.view
|
|
189
|
+
ws.sheet_view.zoomScale = template_ws.sheet_view.zoomScale
|
|
190
|
+
ws.sheet_view.zoomScaleNormal = template_ws.sheet_view.zoomScaleNormal
|
|
191
|
+
ws.sheet_view.showGridLines = template_ws.sheet_view.showGridLines
|
|
192
|
+
return
|
|
193
|
+
ws.print_area = "A1:F%d" % max(end_row, 5)
|
|
194
|
+
ws.page_setup.orientation = "portrait"
|
|
195
|
+
ws.page_setup.paperSize = 9
|
|
196
|
+
ws.page_setup.fitToWidth = 1
|
|
197
|
+
ws.page_setup.fitToHeight = 0
|
|
198
|
+
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
|
199
|
+
ws.sheet_view.view = "pageBreakPreview"
|
|
200
|
+
ws.sheet_view.zoomScale = 100
|
|
201
|
+
ws.sheet_view.zoomScaleNormal = 100
|
|
202
|
+
ws.sheet_view.showGridLines = False
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _copy_style(src, dest):
|
|
206
|
+
dest.font = copy(src.font)
|
|
207
|
+
dest.border = copy(src.border)
|
|
208
|
+
dest.fill = copy(src.fill)
|
|
209
|
+
dest.number_format = copy(src.number_format)
|
|
210
|
+
dest.protection = copy(src.protection)
|
|
211
|
+
dest.alignment = copy(src.alignment)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _write_sheet(ws, rows, ensure_format, template_ws=None):
|
|
215
|
+
if ws.max_row >= 5:
|
|
216
|
+
for row in ws.iter_rows(min_row=5, max_row=ws.max_row, min_col=2, max_col=5):
|
|
217
|
+
for cell in row:
|
|
218
|
+
cell.value = None
|
|
219
|
+
style_row = 5 if ws.max_row >= 5 else None
|
|
220
|
+
style_cells = {}
|
|
221
|
+
if style_row:
|
|
222
|
+
for col in range(2, 6):
|
|
223
|
+
style_cells[col] = ws.cell(row=style_row, column=col)
|
|
224
|
+
template_height = None
|
|
225
|
+
if template_ws is not None:
|
|
226
|
+
template_height = template_ws.row_dimensions[5].height
|
|
227
|
+
styles = _default_styles() if ensure_format else None
|
|
228
|
+
for idx, (cksum, size, path) in enumerate(rows, 5):
|
|
229
|
+
b = ws.cell(row=idx, column=2, value=cksum)
|
|
230
|
+
c = ws.cell(row=idx, column=3, value=size)
|
|
231
|
+
d = ws.cell(row=idx, column=4, value=path)
|
|
232
|
+
e = ws.cell(row=idx, column=5, value="")
|
|
233
|
+
if style_cells:
|
|
234
|
+
_copy_style(style_cells[2], b)
|
|
235
|
+
_copy_style(style_cells[3], c)
|
|
236
|
+
_copy_style(style_cells[4], d)
|
|
237
|
+
_copy_style(style_cells[5], e)
|
|
238
|
+
if ensure_format and styles:
|
|
239
|
+
b.border = _border(styles["medium"], styles["hair"], styles["hair"], None)
|
|
240
|
+
c.border = _border(styles["hair"], styles["hair"], styles["hair"], None)
|
|
241
|
+
d.border = _border(styles["hair"], styles["hair"], styles["hair"], None)
|
|
242
|
+
e.border = _border(styles["hair"], styles["medium"], styles["hair"], None)
|
|
243
|
+
for cell in (b, c, d, e):
|
|
244
|
+
cell.font = styles["base_font"]
|
|
245
|
+
cell.alignment = Alignment(vertical="center")
|
|
246
|
+
if template_height is not None:
|
|
247
|
+
ws.row_dimensions[idx].height = template_height
|
|
248
|
+
end_row = max(5, 4 + len(rows))
|
|
249
|
+
_apply_table_border(ws, 4, end_row, ensure_format)
|
|
250
|
+
if ensure_format:
|
|
251
|
+
blank_row = end_row + 1
|
|
252
|
+
for col in range(2, 6):
|
|
253
|
+
cell = ws.cell(row=blank_row, column=col)
|
|
254
|
+
cell.value = None
|
|
255
|
+
cell.border = Border()
|
|
256
|
+
cell.fill = PatternFill(fill_type=None)
|
|
257
|
+
_configure_page(ws, end_row + 1, template_ws=template_ws)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def main(argv=None):
|
|
261
|
+
argv = argv if argv is not None else sys.argv[1:]
|
|
262
|
+
parser = argparse.ArgumentParser(description="Export cksum results into an Excel template.")
|
|
263
|
+
parser.add_argument("--config", help="pkgmgr main config path")
|
|
264
|
+
parser.add_argument("--pkg-id", required=True, help="pkg id (resolved via pkg_release_root)")
|
|
265
|
+
parser.add_argument("--excel", required=True, help="output xlsx path")
|
|
266
|
+
parser.add_argument("--template", help="xlsx template path (optional)")
|
|
267
|
+
args = parser.parse_args(argv)
|
|
268
|
+
|
|
269
|
+
if openpyxl is None:
|
|
270
|
+
print("[export_cksum] openpyxl is required (pip install openpyxl)")
|
|
271
|
+
return 1
|
|
272
|
+
|
|
273
|
+
pkg_dir = None
|
|
274
|
+
config_path = args.config or os.environ.get("PKGMGR_CONFIG")
|
|
275
|
+
if args.pkg_id:
|
|
276
|
+
pkg_dir = _resolve_pkg_dir(args.pkg_id, config_path=config_path)
|
|
277
|
+
pkg_yaml = _load_pkg_yaml(pkg_dir)
|
|
278
|
+
if not pkg_yaml:
|
|
279
|
+
print("[export_cksum] pkg.yaml not specified; use --pkg-id")
|
|
280
|
+
return 1
|
|
281
|
+
pkg_yaml = os.path.abspath(os.path.expanduser(pkg_yaml))
|
|
282
|
+
pkg_dir = os.path.dirname(pkg_yaml)
|
|
283
|
+
|
|
284
|
+
pkg_cfg = config.load_pkg_config(pkg_yaml)
|
|
285
|
+
releases = (pkg_cfg.get("include") or {}).get("releases") or []
|
|
286
|
+
grouped = _group_release_paths(pkg_dir, releases)
|
|
287
|
+
if not grouped:
|
|
288
|
+
print("[export_cksum] no release entries found in %s" % pkg_yaml)
|
|
289
|
+
return 1
|
|
290
|
+
|
|
291
|
+
excel_path = args.excel
|
|
292
|
+
if not excel_path.lower().endswith(".xlsx"):
|
|
293
|
+
excel_path = excel_path + ".xlsx"
|
|
294
|
+
if os.sep not in excel_path:
|
|
295
|
+
export_dir = os.path.join(pkg_dir, "export")
|
|
296
|
+
excel_path = os.path.join(export_dir, excel_path)
|
|
297
|
+
template_path = args.template or excel_path
|
|
298
|
+
template_available = template_path and os.path.exists(template_path)
|
|
299
|
+
if template_available:
|
|
300
|
+
wb = openpyxl.load_workbook(template_path)
|
|
301
|
+
print("[export_cksum] template loaded: %s" % template_path)
|
|
302
|
+
else:
|
|
303
|
+
wb = openpyxl.Workbook()
|
|
304
|
+
for name in list(wb.sheetnames):
|
|
305
|
+
wb.remove(wb[name])
|
|
306
|
+
print("[export_cksum] template not found; creating new workbook: %s" % excel_path)
|
|
307
|
+
template_ws = wb[wb.sheetnames[0]] if wb.sheetnames else None
|
|
308
|
+
template_title = template_ws.title if template_ws is not None else None
|
|
309
|
+
roots = []
|
|
310
|
+
for root, entries in sorted(grouped.items()):
|
|
311
|
+
roots.append(root)
|
|
312
|
+
root_dir = os.path.join(pkg_dir, root)
|
|
313
|
+
files = _collect_files(root_dir, entries)
|
|
314
|
+
rows = []
|
|
315
|
+
for path in files:
|
|
316
|
+
if not os.path.isfile(path):
|
|
317
|
+
continue
|
|
318
|
+
try:
|
|
319
|
+
relpath = os.path.relpath(path, root_dir)
|
|
320
|
+
except Exception:
|
|
321
|
+
relpath = os.path.basename(path)
|
|
322
|
+
cksum_row = _cksum(path)
|
|
323
|
+
if not cksum_row:
|
|
324
|
+
continue
|
|
325
|
+
rows.append((cksum_row[0], cksum_row[1], relpath))
|
|
326
|
+
rows.sort(key=lambda r: r[2])
|
|
327
|
+
|
|
328
|
+
ensure_format = not template_available
|
|
329
|
+
if root in wb.sheetnames:
|
|
330
|
+
ws = wb[root]
|
|
331
|
+
elif template_ws is not None:
|
|
332
|
+
ws = wb.copy_worksheet(template_ws)
|
|
333
|
+
ws.title = root
|
|
334
|
+
else:
|
|
335
|
+
ws = wb.create_sheet(title=root)
|
|
336
|
+
_init_sheet(ws, root, "PKG Release 경로 작성 필요", ensure_format)
|
|
337
|
+
_write_sheet(ws, rows, ensure_format, template_ws=template_ws)
|
|
338
|
+
print("[export_cksum] sheet=%s rows=%d" % (root, len(rows)))
|
|
339
|
+
if template_title and template_title not in roots and template_title in wb.sheetnames:
|
|
340
|
+
wb.remove(wb[template_title])
|
|
341
|
+
|
|
342
|
+
out_path = excel_path
|
|
343
|
+
if not out_path.lower().endswith(".xlsx"):
|
|
344
|
+
out_path = out_path + ".xlsx"
|
|
345
|
+
out_dir = os.path.dirname(os.path.abspath(out_path))
|
|
346
|
+
if out_dir and not os.path.exists(out_dir):
|
|
347
|
+
os.makedirs(out_dir)
|
|
348
|
+
wb.save(out_path)
|
|
349
|
+
print("[export_cksum] wrote %s" % out_path)
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
sys.exit(main())
|