kspl 0.1.0__tar.gz
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.
- kspl-0.1.0/LICENSE +22 -0
- kspl-0.1.0/PKG-INFO +125 -0
- kspl-0.1.0/README.md +97 -0
- kspl-0.1.0/pyproject.toml +113 -0
- kspl-0.1.0/src/kspl/__init__.py +1 -0
- kspl-0.1.0/src/kspl/_run.py +9 -0
- kspl-0.1.0/src/kspl/generate.py +194 -0
- kspl-0.1.0/src/kspl/gui.py +338 -0
- kspl-0.1.0/src/kspl/kconfig.py +221 -0
- kspl-0.1.0/src/kspl/main.py +38 -0
- kspl-0.1.0/src/kspl/py.typed +0 -0
kspl-0.1.0/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
MIT License
|
3
|
+
|
4
|
+
Copyright (c) 2023 Cuinixam
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
8
|
+
in the Software without restriction, including without limitation the rights
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
11
|
+
furnished to do so, subject to the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
14
|
+
copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
SOFTWARE.
|
kspl-0.1.0/PKG-INFO
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: kspl
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: KConfig GUI for Software Product Lines with multiple variants.
|
5
|
+
Home-page: https://github.com/cuinixam/kspl
|
6
|
+
License: MIT
|
7
|
+
Author: Cuinixam
|
8
|
+
Author-email: cuinixam@me.com
|
9
|
+
Requires-Python: >=3.10,<4.0
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
11
|
+
Classifier: Intended Audience :: Developers
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
13
|
+
Classifier: Natural Language :: English
|
14
|
+
Classifier: Operating System :: OS Independent
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
19
|
+
Requires-Dist: customtkinter (>=5.2.0,<6.0.0)
|
20
|
+
Requires-Dist: kconfiglib (>=14.1.0,<15.0.0)
|
21
|
+
Requires-Dist: py-app-dev (>=1.0.0,<2.0.0)
|
22
|
+
Project-URL: Bug Tracker, https://github.com/cuinixam/kspl/issues
|
23
|
+
Project-URL: Changelog, https://github.com/cuinixam/kspl/blob/main/CHANGELOG.md
|
24
|
+
Project-URL: Documentation, https://kspl.readthedocs.io
|
25
|
+
Project-URL: Repository, https://github.com/cuinixam/kspl
|
26
|
+
Description-Content-Type: text/markdown
|
27
|
+
|
28
|
+
# SPL KConfig GUI
|
29
|
+
|
30
|
+
<p align="center">
|
31
|
+
<a href="https://github.com/cuinixam/kspl/actions/workflows/ci.yml?query=branch%3Amain">
|
32
|
+
<img src="https://img.shields.io/github/actions/workflow/status/cuinixam/kspl/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
33
|
+
</a>
|
34
|
+
<a href="https://kspl.readthedocs.io">
|
35
|
+
<img src="https://img.shields.io/readthedocs/kspl.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
36
|
+
</a>
|
37
|
+
<a href="https://codecov.io/gh/cuinixam/kspl">
|
38
|
+
<img src="https://img.shields.io/codecov/c/github/cuinixam/kspl.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
39
|
+
</a>
|
40
|
+
</p>
|
41
|
+
<p align="center">
|
42
|
+
<a href="https://python-poetry.org/">
|
43
|
+
<img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
|
44
|
+
</a>
|
45
|
+
<a href="https://github.com/ambv/black">
|
46
|
+
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">
|
47
|
+
</a>
|
48
|
+
<a href="https://github.com/pre-commit/pre-commit">
|
49
|
+
<img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
|
50
|
+
</a>
|
51
|
+
</p>
|
52
|
+
<p align="center">
|
53
|
+
<a href="https://pypi.org/project/kspl/">
|
54
|
+
<img src="https://img.shields.io/pypi/v/kspl.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
55
|
+
</a>
|
56
|
+
<img src="https://img.shields.io/pypi/pyversions/kspl.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
57
|
+
<img src="https://img.shields.io/pypi/l/kspl.svg?style=flat-square" alt="License">
|
58
|
+
</p>
|
59
|
+
|
60
|
+
KConfig GUI for Software Product Lines with multiple variants.
|
61
|
+
|
62
|
+
## Installation
|
63
|
+
|
64
|
+
Install this via pip (or your favourite package manager):
|
65
|
+
|
66
|
+
`pip install kspl`
|
67
|
+
|
68
|
+
## Start developing
|
69
|
+
|
70
|
+
The project uses Poetry for dependencies management and packaging.
|
71
|
+
If you do not have Poetry installed, you can run the `boostrap.ps1` script.
|
72
|
+
This will install Python and Poetry as configured in `scoopfile.json`.
|
73
|
+
|
74
|
+
```powershell
|
75
|
+
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser -Force
|
76
|
+
.\bootstrap.ps1
|
77
|
+
```
|
78
|
+
|
79
|
+
To install the development dependencies in a virtual environment, type:
|
80
|
+
|
81
|
+
```shell
|
82
|
+
poetry install
|
83
|
+
```
|
84
|
+
|
85
|
+
This will also generate a `poetry.lock` file, you should track this file in version control. To execute the test suite, call pytest inside Poetry's virtual environment via `poetry run`:
|
86
|
+
|
87
|
+
```shell
|
88
|
+
poetry run pytest
|
89
|
+
```
|
90
|
+
|
91
|
+
Check out the Poetry documentation for more information on the available commands.
|
92
|
+
|
93
|
+
For those using [VS Code](https://code.visualstudio.com/) there are tasks defined for the most common commands:
|
94
|
+
|
95
|
+
- install development dependencies
|
96
|
+
- run tests
|
97
|
+
- run all checks configured for pre-commit
|
98
|
+
- generate documentation
|
99
|
+
|
100
|
+
See the `.vscode/tasks.json` for more details.
|
101
|
+
|
102
|
+
## Committing changes
|
103
|
+
|
104
|
+
This repository uses [commitlint](https://github.com/conventional-changelog/commitlint) for checking if the commit message meets the [conventional commit format](https://www.conventionalcommits.org/en).
|
105
|
+
|
106
|
+
## Contributors ✨
|
107
|
+
|
108
|
+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
109
|
+
|
110
|
+
<!-- prettier-ignore-start -->
|
111
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
112
|
+
<!-- markdownlint-disable -->
|
113
|
+
<!-- markdownlint-enable -->
|
114
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
115
|
+
<!-- prettier-ignore-end -->
|
116
|
+
|
117
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
118
|
+
|
119
|
+
## Credits
|
120
|
+
|
121
|
+
This package was created with
|
122
|
+
[Copier](https://copier.readthedocs.io/) and the
|
123
|
+
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
|
124
|
+
project template.
|
125
|
+
|
kspl-0.1.0/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# SPL KConfig GUI
|
2
|
+
|
3
|
+
<p align="center">
|
4
|
+
<a href="https://github.com/cuinixam/kspl/actions/workflows/ci.yml?query=branch%3Amain">
|
5
|
+
<img src="https://img.shields.io/github/actions/workflow/status/cuinixam/kspl/ci.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
|
6
|
+
</a>
|
7
|
+
<a href="https://kspl.readthedocs.io">
|
8
|
+
<img src="https://img.shields.io/readthedocs/kspl.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
|
9
|
+
</a>
|
10
|
+
<a href="https://codecov.io/gh/cuinixam/kspl">
|
11
|
+
<img src="https://img.shields.io/codecov/c/github/cuinixam/kspl.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
|
12
|
+
</a>
|
13
|
+
</p>
|
14
|
+
<p align="center">
|
15
|
+
<a href="https://python-poetry.org/">
|
16
|
+
<img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
|
17
|
+
</a>
|
18
|
+
<a href="https://github.com/ambv/black">
|
19
|
+
<img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square" alt="black">
|
20
|
+
</a>
|
21
|
+
<a href="https://github.com/pre-commit/pre-commit">
|
22
|
+
<img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
|
23
|
+
</a>
|
24
|
+
</p>
|
25
|
+
<p align="center">
|
26
|
+
<a href="https://pypi.org/project/kspl/">
|
27
|
+
<img src="https://img.shields.io/pypi/v/kspl.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
|
28
|
+
</a>
|
29
|
+
<img src="https://img.shields.io/pypi/pyversions/kspl.svg?style=flat-square&logo=python&logoColor=fff" alt="Supported Python versions">
|
30
|
+
<img src="https://img.shields.io/pypi/l/kspl.svg?style=flat-square" alt="License">
|
31
|
+
</p>
|
32
|
+
|
33
|
+
KConfig GUI for Software Product Lines with multiple variants.
|
34
|
+
|
35
|
+
## Installation
|
36
|
+
|
37
|
+
Install this via pip (or your favourite package manager):
|
38
|
+
|
39
|
+
`pip install kspl`
|
40
|
+
|
41
|
+
## Start developing
|
42
|
+
|
43
|
+
The project uses Poetry for dependencies management and packaging.
|
44
|
+
If you do not have Poetry installed, you can run the `boostrap.ps1` script.
|
45
|
+
This will install Python and Poetry as configured in `scoopfile.json`.
|
46
|
+
|
47
|
+
```powershell
|
48
|
+
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser -Force
|
49
|
+
.\bootstrap.ps1
|
50
|
+
```
|
51
|
+
|
52
|
+
To install the development dependencies in a virtual environment, type:
|
53
|
+
|
54
|
+
```shell
|
55
|
+
poetry install
|
56
|
+
```
|
57
|
+
|
58
|
+
This will also generate a `poetry.lock` file, you should track this file in version control. To execute the test suite, call pytest inside Poetry's virtual environment via `poetry run`:
|
59
|
+
|
60
|
+
```shell
|
61
|
+
poetry run pytest
|
62
|
+
```
|
63
|
+
|
64
|
+
Check out the Poetry documentation for more information on the available commands.
|
65
|
+
|
66
|
+
For those using [VS Code](https://code.visualstudio.com/) there are tasks defined for the most common commands:
|
67
|
+
|
68
|
+
- install development dependencies
|
69
|
+
- run tests
|
70
|
+
- run all checks configured for pre-commit
|
71
|
+
- generate documentation
|
72
|
+
|
73
|
+
See the `.vscode/tasks.json` for more details.
|
74
|
+
|
75
|
+
## Committing changes
|
76
|
+
|
77
|
+
This repository uses [commitlint](https://github.com/conventional-changelog/commitlint) for checking if the commit message meets the [conventional commit format](https://www.conventionalcommits.org/en).
|
78
|
+
|
79
|
+
## Contributors ✨
|
80
|
+
|
81
|
+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
82
|
+
|
83
|
+
<!-- prettier-ignore-start -->
|
84
|
+
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
85
|
+
<!-- markdownlint-disable -->
|
86
|
+
<!-- markdownlint-enable -->
|
87
|
+
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
88
|
+
<!-- prettier-ignore-end -->
|
89
|
+
|
90
|
+
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
|
91
|
+
|
92
|
+
## Credits
|
93
|
+
|
94
|
+
This package was created with
|
95
|
+
[Copier](https://copier.readthedocs.io/) and the
|
96
|
+
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
|
97
|
+
project template.
|
@@ -0,0 +1,113 @@
|
|
1
|
+
[tool.poetry]
|
2
|
+
name = "kspl"
|
3
|
+
version = "0.1.0"
|
4
|
+
description = "KConfig GUI for Software Product Lines with multiple variants."
|
5
|
+
authors = ["Cuinixam <cuinixam@me.com>"]
|
6
|
+
license = "MIT"
|
7
|
+
readme = "README.md"
|
8
|
+
repository = "https://github.com/cuinixam/kspl"
|
9
|
+
documentation = "https://kspl.readthedocs.io"
|
10
|
+
classifiers = [
|
11
|
+
"Development Status :: 2 - Pre-Alpha",
|
12
|
+
"Intended Audience :: Developers",
|
13
|
+
"Natural Language :: English",
|
14
|
+
"Operating System :: OS Independent",
|
15
|
+
"Topic :: Software Development :: Libraries",
|
16
|
+
]
|
17
|
+
packages = [
|
18
|
+
{ include = "kspl", from = "src" },
|
19
|
+
]
|
20
|
+
|
21
|
+
[tool.poetry.scripts]
|
22
|
+
kspl = "kspl.main:main"
|
23
|
+
|
24
|
+
[tool.poetry.urls]
|
25
|
+
"Bug Tracker" = "https://github.com/cuinixam/kspl/issues"
|
26
|
+
"Changelog" = "https://github.com/cuinixam/kspl/blob/main/CHANGELOG.md"
|
27
|
+
|
28
|
+
[tool.poetry.dependencies]
|
29
|
+
python = "^3.10"
|
30
|
+
customtkinter = "^5.2.0"
|
31
|
+
kconfiglib = "^14.1.0"
|
32
|
+
py-app-dev = "^1.0.0"
|
33
|
+
|
34
|
+
[tool.poetry.group.dev.dependencies]
|
35
|
+
pytest = "^7.0"
|
36
|
+
pytest-cov = "^4.0"
|
37
|
+
black = "^23.1.0"
|
38
|
+
pre-commit = "^3.1.1"
|
39
|
+
|
40
|
+
[tool.poetry.group.docs]
|
41
|
+
optional = true
|
42
|
+
|
43
|
+
[tool.poetry.group.docs.dependencies]
|
44
|
+
myst-parser = ">=0.16"
|
45
|
+
sphinx = ">=4.0"
|
46
|
+
sphinx-rtd-theme = ">=1.0"
|
47
|
+
m2r = "^0.3.1"
|
48
|
+
sphinxcontrib-mermaid = "^0.8.1"
|
49
|
+
|
50
|
+
[tool.semantic_release]
|
51
|
+
branch = "main"
|
52
|
+
version_toml = "pyproject.toml:tool.poetry.version"
|
53
|
+
version_variable = "src/kspl/__init__.py:__version__"
|
54
|
+
build_command = "pip install poetry && poetry build"
|
55
|
+
|
56
|
+
[tool.semantic_release.changelog]
|
57
|
+
exclude_commit_patterns = [
|
58
|
+
"chore*",
|
59
|
+
"ci*",
|
60
|
+
]
|
61
|
+
|
62
|
+
[tool.semantic_release.changelog.environment]
|
63
|
+
keep_trailing_newline = true
|
64
|
+
|
65
|
+
[tool.pytest.ini_options]
|
66
|
+
addopts = "-v -Wdefault --cov=kspl --cov-report=term-missing:skip-covered -s"
|
67
|
+
pythonpath = ["src"]
|
68
|
+
|
69
|
+
[tool.coverage.run]
|
70
|
+
branch = true
|
71
|
+
|
72
|
+
[tool.coverage.report]
|
73
|
+
exclude_lines = [
|
74
|
+
"pragma: no cover",
|
75
|
+
"@overload",
|
76
|
+
"if TYPE_CHECKING",
|
77
|
+
"raise NotImplementedError",
|
78
|
+
'if __name__ == "__main__":',
|
79
|
+
]
|
80
|
+
|
81
|
+
[tool.isort]
|
82
|
+
profile = "black"
|
83
|
+
known_first_party = ["kspl", "tests"]
|
84
|
+
|
85
|
+
[tool.mypy]
|
86
|
+
check_untyped_defs = true
|
87
|
+
disallow_any_generics = true
|
88
|
+
disallow_incomplete_defs = true
|
89
|
+
disallow_untyped_defs = true
|
90
|
+
mypy_path = "src/"
|
91
|
+
no_implicit_optional = true
|
92
|
+
show_error_codes = true
|
93
|
+
warn_unreachable = true
|
94
|
+
warn_unused_ignores = true
|
95
|
+
exclude = [
|
96
|
+
'docs/.*',
|
97
|
+
'setup.py',
|
98
|
+
]
|
99
|
+
|
100
|
+
[[tool.mypy.overrides]]
|
101
|
+
module = "tests.*"
|
102
|
+
allow_untyped_defs = true
|
103
|
+
|
104
|
+
[[tool.mypy.overrides]]
|
105
|
+
module = "docs.*"
|
106
|
+
ignore_errors = true
|
107
|
+
|
108
|
+
[tool.codespell]
|
109
|
+
skip = '*.lock'
|
110
|
+
|
111
|
+
[build-system]
|
112
|
+
requires = ["poetry-core>=1.0.0"]
|
113
|
+
build-backend = "poetry.core.masonry.api"
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.1.0"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
"""Used to run main from the command line when run from this repository.
|
2
|
+
This is required because kspl module is not visible when running
|
3
|
+
from the repository."""
|
4
|
+
import runpy
|
5
|
+
import sys
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
sys.path.insert(0, Path(__file__).parent.parent.absolute().as_posix())
|
9
|
+
runpy.run_module("kspl.main", run_name="__main__")
|
@@ -0,0 +1,194 @@
|
|
1
|
+
import json
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from argparse import ArgumentParser, Namespace
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import List, Optional
|
7
|
+
|
8
|
+
import kconfiglib
|
9
|
+
from mashumaro import DataClassDictMixin
|
10
|
+
from py_app_dev.core.cmd_line import Command, register_arguments_for_config_dataclass
|
11
|
+
from py_app_dev.core.logging import logger, time_it
|
12
|
+
|
13
|
+
from kspl.kconfig import ConfigElementType, ConfigurationData, KConfig, TriState
|
14
|
+
|
15
|
+
|
16
|
+
class GeneratedFile:
|
17
|
+
def __init__(
|
18
|
+
self, path: Path, content: str = "", skip_writing_if_unchanged: bool = False
|
19
|
+
) -> None:
|
20
|
+
self.path = path
|
21
|
+
|
22
|
+
self.content = content
|
23
|
+
|
24
|
+
self.skip_writing_if_unchanged = skip_writing_if_unchanged
|
25
|
+
|
26
|
+
def to_string(self) -> str:
|
27
|
+
return self.content
|
28
|
+
|
29
|
+
def to_file(self) -> None:
|
30
|
+
"""Only write to file if the content has changed.
|
31
|
+
The directory of the file is created if it does not exist."""
|
32
|
+
|
33
|
+
content = self.to_string()
|
34
|
+
|
35
|
+
if (
|
36
|
+
not self.path.exists()
|
37
|
+
or not self.skip_writing_if_unchanged
|
38
|
+
or self.path.read_text() != content
|
39
|
+
):
|
40
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
41
|
+
self.path.write_text(content)
|
42
|
+
|
43
|
+
|
44
|
+
class FileWriter(ABC):
|
45
|
+
"""- writes the ConfigurationData to a file"""
|
46
|
+
|
47
|
+
def __init__(self, output_file: Path):
|
48
|
+
self.output_file = output_file
|
49
|
+
|
50
|
+
def write(self, configuration_data: ConfigurationData) -> None:
|
51
|
+
"""- writes the ConfigurationData to a file
|
52
|
+
The file shall not be modified if the content is the same as the existing one"""
|
53
|
+
content = self.generate_content(configuration_data)
|
54
|
+
GeneratedFile(
|
55
|
+
self.output_file, content, skip_writing_if_unchanged=True
|
56
|
+
).to_file()
|
57
|
+
|
58
|
+
@abstractmethod
|
59
|
+
def generate_content(self, configuration_data: ConfigurationData) -> str:
|
60
|
+
"""- generates the content of the file from the ConfigurationData"""
|
61
|
+
|
62
|
+
|
63
|
+
class HeaderWriter(FileWriter):
|
64
|
+
"""Writes the ConfigurationData as pre-processor defines in a C Header file"""
|
65
|
+
|
66
|
+
config_prefix = "CONFIG_" # Prefix for all configuration defines
|
67
|
+
|
68
|
+
def generate_content(self, configuration_data: ConfigurationData) -> str:
|
69
|
+
"""This method does exactly what the kconfiglib.write_autoconf() method does.
|
70
|
+
We had to implemented here because we refactor the file writers to use the ConfigurationData
|
71
|
+
instead of the KConfig configuration. ConfigurationData has variable substitution already done.
|
72
|
+
"""
|
73
|
+
result: List[str] = [
|
74
|
+
"/** @file */",
|
75
|
+
"#ifndef __autoconf_h__",
|
76
|
+
"#define __autoconf_h__",
|
77
|
+
"",
|
78
|
+
]
|
79
|
+
|
80
|
+
def add_define(define_decl: str, description: str) -> None:
|
81
|
+
result.append(f"/** {description} */")
|
82
|
+
result.append(define_decl)
|
83
|
+
|
84
|
+
for element in configuration_data.elements:
|
85
|
+
val = element.value
|
86
|
+
if element.type in [ConfigElementType.BOOL, ConfigElementType.TRISTATE]:
|
87
|
+
if val == TriState.Y:
|
88
|
+
add_define(
|
89
|
+
f"#define {self.config_prefix}{element.name} 1",
|
90
|
+
element.name,
|
91
|
+
)
|
92
|
+
elif val == TriState.M:
|
93
|
+
add_define(
|
94
|
+
"#define {}{}_MODULE 1".format(
|
95
|
+
self.config_prefix, element.name
|
96
|
+
),
|
97
|
+
element.name,
|
98
|
+
)
|
99
|
+
|
100
|
+
elif element.type is ConfigElementType.STRING:
|
101
|
+
add_define(
|
102
|
+
'#define {}{} "{}"'.format(
|
103
|
+
self.config_prefix, element.name, kconfiglib.escape(val)
|
104
|
+
),
|
105
|
+
element.name,
|
106
|
+
)
|
107
|
+
|
108
|
+
else: # element.type in [INT, HEX]:
|
109
|
+
if element.type is ConfigElementType.HEX:
|
110
|
+
val = hex(val)
|
111
|
+
add_define(
|
112
|
+
f"#define {self.config_prefix}{element.name} {val}",
|
113
|
+
element.name,
|
114
|
+
)
|
115
|
+
result.extend(["", "#endif /* __autoconf_h__ */", ""])
|
116
|
+
return "\n".join(result)
|
117
|
+
|
118
|
+
|
119
|
+
class JsonWriter(FileWriter):
|
120
|
+
"""Writes the ConfigurationData in json format"""
|
121
|
+
|
122
|
+
def generate_content(self, configuration_data: ConfigurationData) -> str:
|
123
|
+
result = {}
|
124
|
+
for element in configuration_data.elements:
|
125
|
+
if element.type is ConfigElementType.BOOL:
|
126
|
+
result[element.name] = True if element.value == TriState.Y else False
|
127
|
+
else:
|
128
|
+
result[element.name] = element.value
|
129
|
+
return json.dumps({"features": result}, indent=4)
|
130
|
+
|
131
|
+
|
132
|
+
class CMakeWriter(FileWriter):
|
133
|
+
"""Writes the ConfigurationData as CMake variables"""
|
134
|
+
|
135
|
+
def generate_content(self, configuration_data: ConfigurationData) -> str:
|
136
|
+
result: List[str] = []
|
137
|
+
add = result.append
|
138
|
+
for element in configuration_data.elements:
|
139
|
+
val = element.value
|
140
|
+
if element.type is ConfigElementType.BOOL:
|
141
|
+
val = True if element.value == TriState.Y else False
|
142
|
+
add(f'set({element.name} "{val}")')
|
143
|
+
|
144
|
+
return "\n".join(result)
|
145
|
+
|
146
|
+
|
147
|
+
@dataclass
|
148
|
+
class GenerateCommandConfig(DataClassDictMixin):
|
149
|
+
kconfig_model_file: Path = field(metadata={"help": "KConfig model file (KConfig)."})
|
150
|
+
kconfig_config_file: Optional[Path] = field(
|
151
|
+
default=None, metadata={"help": "KConfig user configuration file (config.txt)."}
|
152
|
+
)
|
153
|
+
out_header_file: Optional[Path] = field(
|
154
|
+
default=None, metadata={"help": "File to write the configuration as C header."}
|
155
|
+
)
|
156
|
+
out_json_file: Optional[Path] = field(
|
157
|
+
default=None,
|
158
|
+
metadata={"help": "File to write the configuration in JSON format."},
|
159
|
+
)
|
160
|
+
out_cmake_file: Optional[Path] = field(
|
161
|
+
default=None,
|
162
|
+
metadata={"help": "File to write the configuration in CMake format."},
|
163
|
+
)
|
164
|
+
|
165
|
+
@classmethod
|
166
|
+
def from_namespace(cls, namespace: Namespace) -> "GenerateCommandConfig":
|
167
|
+
return cls.from_dict(vars(namespace))
|
168
|
+
|
169
|
+
|
170
|
+
class GenerateCommand(Command):
|
171
|
+
def __init__(self) -> None:
|
172
|
+
super().__init__(
|
173
|
+
"generate", "Generate the KConfig configuration in the specified formats."
|
174
|
+
)
|
175
|
+
self.logger = logger.bind()
|
176
|
+
|
177
|
+
@time_it("Build")
|
178
|
+
def run(self, args: Namespace) -> int:
|
179
|
+
self.logger.info(f"Running {self.name} with args {args}")
|
180
|
+
cmd_config = GenerateCommandConfig.from_namespace(args)
|
181
|
+
config = KConfig(
|
182
|
+
cmd_config.kconfig_model_file, cmd_config.kconfig_config_file
|
183
|
+
).config
|
184
|
+
|
185
|
+
if cmd_config.out_header_file:
|
186
|
+
HeaderWriter(cmd_config.out_header_file).write(config)
|
187
|
+
if cmd_config.out_json_file:
|
188
|
+
JsonWriter(cmd_config.out_json_file).write(config)
|
189
|
+
if cmd_config.out_cmake_file:
|
190
|
+
CMakeWriter(cmd_config.out_cmake_file).write(config)
|
191
|
+
return 0
|
192
|
+
|
193
|
+
def _register_arguments(self, parser: ArgumentParser) -> None:
|
194
|
+
register_arguments_for_config_dataclass(parser, GenerateCommandConfig)
|
@@ -0,0 +1,338 @@
|
|
1
|
+
import tkinter
|
2
|
+
from abc import abstractmethod
|
3
|
+
from argparse import ArgumentParser, Namespace
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from enum import Enum, auto
|
6
|
+
from pathlib import Path
|
7
|
+
from tkinter import simpledialog, ttk
|
8
|
+
from typing import Any, Dict, List
|
9
|
+
|
10
|
+
import customtkinter
|
11
|
+
from mashumaro import DataClassDictMixin
|
12
|
+
from py_app_dev.core.cmd_line import Command, register_arguments_for_config_dataclass
|
13
|
+
from py_app_dev.core.logging import logger, time_it
|
14
|
+
from py_app_dev.mvp.event_manager import EventID, EventManager
|
15
|
+
from py_app_dev.mvp.presenter import Presenter
|
16
|
+
from py_app_dev.mvp.view import View
|
17
|
+
|
18
|
+
from kspl.kconfig import KConfig
|
19
|
+
|
20
|
+
|
21
|
+
class ConfigElementType(Enum):
|
22
|
+
MENU = auto()
|
23
|
+
CONFIG = auto()
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class ConfigElementViewData:
|
28
|
+
name: str
|
29
|
+
level: int
|
30
|
+
type: ConfigElementType
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class VariantViewData:
|
35
|
+
"""A variant is a set of configuration values for a KConfig model."""
|
36
|
+
|
37
|
+
name: str
|
38
|
+
config_dict: Dict[str, int | str | bool]
|
39
|
+
|
40
|
+
|
41
|
+
class KSplEvents(EventID):
|
42
|
+
EDIT = auto()
|
43
|
+
|
44
|
+
|
45
|
+
class CTkView(View):
|
46
|
+
@abstractmethod
|
47
|
+
def mainloop(self) -> None:
|
48
|
+
pass
|
49
|
+
|
50
|
+
|
51
|
+
class MainView(CTkView):
|
52
|
+
def __init__(
|
53
|
+
self,
|
54
|
+
event_manager: EventManager,
|
55
|
+
elements: List[ConfigElementViewData],
|
56
|
+
variants: List[VariantViewData],
|
57
|
+
) -> None:
|
58
|
+
self.event_manager = event_manager
|
59
|
+
self.elements = elements
|
60
|
+
self.variants = variants
|
61
|
+
self.root = customtkinter.CTk()
|
62
|
+
|
63
|
+
# Configure the main window
|
64
|
+
self.root.title("K-SPL")
|
65
|
+
self.root.geometry(f"{1080}x{580}")
|
66
|
+
|
67
|
+
# ========================================================
|
68
|
+
# create tabview and populate with frames
|
69
|
+
tabview = customtkinter.CTkTabview(self.root)
|
70
|
+
self.tree = self.create_tree_view(tabview.add("Configuration"))
|
71
|
+
self.tree["columns"] = tuple(variant.name for variant in self.variants)
|
72
|
+
self.tree.heading("#0", text="Configuration")
|
73
|
+
for variant in self.variants:
|
74
|
+
self.tree.heading(variant.name, text=variant.name)
|
75
|
+
# Keep track of the mapping between the tree view items and the config elements
|
76
|
+
self.tree_view_items_mapping = self.populate_tree_view()
|
77
|
+
self.tree.pack(fill="both", expand=True)
|
78
|
+
self.tree.bind("<Double-1>", self.double_click_handler)
|
79
|
+
|
80
|
+
# ========================================================
|
81
|
+
# put all together
|
82
|
+
self.root.grid_columnconfigure(0, weight=1)
|
83
|
+
self.root.grid_rowconfigure(0, weight=1)
|
84
|
+
tabview.grid(row=0, column=0, sticky="nsew")
|
85
|
+
|
86
|
+
def mainloop(self) -> None:
|
87
|
+
self.root.mainloop()
|
88
|
+
|
89
|
+
def create_tree_view(self, frame: customtkinter.CTkFrame) -> ttk.Treeview:
|
90
|
+
frame.grid_rowconfigure(0, weight=10)
|
91
|
+
frame.grid_rowconfigure(1, weight=1)
|
92
|
+
frame.grid_columnconfigure(0, weight=1)
|
93
|
+
|
94
|
+
columns = [var.name for var in self.variants]
|
95
|
+
|
96
|
+
style = ttk.Style()
|
97
|
+
style.configure(
|
98
|
+
"mystyle.Treeview", highlightthickness=0, bd=0, font=("Calibri", 14)
|
99
|
+
) # Modify the font of the body
|
100
|
+
style.configure(
|
101
|
+
"mystyle.Treeview.Heading", font=("Calibri", 14, "bold")
|
102
|
+
) # Modify the font of the headings
|
103
|
+
|
104
|
+
# create a Treeview widget
|
105
|
+
config_treeview = ttk.Treeview(
|
106
|
+
frame,
|
107
|
+
columns=columns,
|
108
|
+
show="tree headings",
|
109
|
+
style="mystyle.Treeview",
|
110
|
+
)
|
111
|
+
config_treeview.grid(row=0, column=0, sticky="nsew")
|
112
|
+
return config_treeview
|
113
|
+
|
114
|
+
def populate_tree_view(self) -> Dict[str, str]:
|
115
|
+
"""
|
116
|
+
Populates the tree view with the configuration elements.
|
117
|
+
:return: a mapping between the tree view items and the configuration elements
|
118
|
+
"""
|
119
|
+
stack = [] # To keep track of the parent items
|
120
|
+
last_level = -1
|
121
|
+
mapping: Dict[str, str] = {}
|
122
|
+
|
123
|
+
for element in self.elements:
|
124
|
+
values = self.collect_values_for_element(element)
|
125
|
+
if element.level == 0:
|
126
|
+
# Insert at the root level
|
127
|
+
item_id = self.tree.insert("", "end", text=element.name, values=values)
|
128
|
+
stack = [item_id] # Reset the stack with the root item
|
129
|
+
elif element.level > last_level:
|
130
|
+
# Insert as a child of the last inserted item
|
131
|
+
item_id = self.tree.insert(
|
132
|
+
stack[-1], "end", text=element.name, values=values
|
133
|
+
)
|
134
|
+
stack.append(item_id)
|
135
|
+
elif element.level == last_level:
|
136
|
+
# Insert at the same level as the last item
|
137
|
+
item_id = self.tree.insert(
|
138
|
+
stack[-2], "end", text=element.name, values=values
|
139
|
+
)
|
140
|
+
stack[-1] = item_id # Replace the top item in the stack
|
141
|
+
else:
|
142
|
+
# Go up in the hierarchy and insert at the appropriate level
|
143
|
+
item_id = self.tree.insert(
|
144
|
+
stack[element.level - 1], "end", text=element.name, values=values
|
145
|
+
)
|
146
|
+
stack = stack[: element.level] + [item_id]
|
147
|
+
|
148
|
+
last_level = element.level
|
149
|
+
mapping[item_id] = element.name
|
150
|
+
return mapping
|
151
|
+
|
152
|
+
def collect_values_for_element(
|
153
|
+
self, element: ConfigElementViewData
|
154
|
+
) -> List[int | str]:
|
155
|
+
return (
|
156
|
+
[
|
157
|
+
self.prepare_value_to_be_displayed(
|
158
|
+
variant.config_dict.get(element.name, None)
|
159
|
+
)
|
160
|
+
for variant in self.variants
|
161
|
+
]
|
162
|
+
if element.type == ConfigElementType.CONFIG
|
163
|
+
else []
|
164
|
+
)
|
165
|
+
|
166
|
+
def prepare_value_to_be_displayed(self, value: Any) -> str:
|
167
|
+
if value is None:
|
168
|
+
return "N/A"
|
169
|
+
elif isinstance(value, bool):
|
170
|
+
return "✅" if value else "⛔"
|
171
|
+
else:
|
172
|
+
return str(value)
|
173
|
+
|
174
|
+
def double_click_handler(self, event: tkinter.Event) -> None: # type: ignore
|
175
|
+
current_selection = self.tree.selection()
|
176
|
+
if not current_selection:
|
177
|
+
return
|
178
|
+
|
179
|
+
selected_item = current_selection[0]
|
180
|
+
elem_name = self.tree_view_items_mapping[selected_item]
|
181
|
+
|
182
|
+
variant_idx_str = self.tree.identify_column(event.x) # Get the clicked column
|
183
|
+
variant_idx = (
|
184
|
+
int(variant_idx_str.split("#")[-1]) - 1
|
185
|
+
) # Convert to 0-based index
|
186
|
+
|
187
|
+
if variant_idx < 0 or variant_idx >= len(self.variants):
|
188
|
+
return
|
189
|
+
|
190
|
+
selected_variant = self.variants[variant_idx]
|
191
|
+
selected_value = selected_variant.config_dict[elem_name]
|
192
|
+
|
193
|
+
if selected_value is not None:
|
194
|
+
new_value = selected_value
|
195
|
+
if isinstance(selected_value, bool):
|
196
|
+
# Toggle the boolean value
|
197
|
+
new_value = not selected_value
|
198
|
+
elif isinstance(selected_value, int):
|
199
|
+
tmp_int_value = simpledialog.askinteger(
|
200
|
+
"Enter new value", "Enter new value", initialvalue=selected_value
|
201
|
+
)
|
202
|
+
if tmp_int_value is not None:
|
203
|
+
new_value = tmp_int_value
|
204
|
+
elif isinstance(selected_value, str):
|
205
|
+
# Prompt the user to enter a new string value using messagebox
|
206
|
+
tmp_str_value = simpledialog.askstring(
|
207
|
+
"Enter new value", "Enter new value", initialvalue=selected_value
|
208
|
+
)
|
209
|
+
if tmp_str_value is not None:
|
210
|
+
new_value = tmp_str_value
|
211
|
+
|
212
|
+
selected_variant.config_dict[elem_name] = new_value
|
213
|
+
|
214
|
+
# Update the Treeview
|
215
|
+
values = list(
|
216
|
+
self.tree.item(selected_item, "values")
|
217
|
+
) # Get the current values of the selected item
|
218
|
+
values[variant_idx] = self.prepare_value_to_be_displayed(new_value)
|
219
|
+
self.tree.item(selected_item, values=values)
|
220
|
+
|
221
|
+
|
222
|
+
@dataclass
|
223
|
+
class VariantData:
|
224
|
+
name: str
|
225
|
+
config: KConfig
|
226
|
+
|
227
|
+
|
228
|
+
class SPLKConfigData:
|
229
|
+
def __init__(self, project_root_dir: Path) -> None:
|
230
|
+
self.project_root_dir = project_root_dir.absolute()
|
231
|
+
variant_config_files = self._search_variant_config_file(self.project_root_dir)
|
232
|
+
self.model = KConfig(self.kconfig_model_file)
|
233
|
+
if variant_config_files:
|
234
|
+
self.variant_configs: List[VariantData] = [
|
235
|
+
VariantData(
|
236
|
+
self._get_variant_name(file), KConfig(self.kconfig_model_file, file)
|
237
|
+
)
|
238
|
+
for file in variant_config_files
|
239
|
+
]
|
240
|
+
else:
|
241
|
+
self.variant_configs = [VariantData("Default", self.model)]
|
242
|
+
|
243
|
+
@property
|
244
|
+
def kconfig_model_file(self) -> Path:
|
245
|
+
return self.project_root_dir / "KConfig"
|
246
|
+
|
247
|
+
def get_elements(self) -> List[ConfigElementViewData]:
|
248
|
+
elements = []
|
249
|
+
for elem in self.model.elements:
|
250
|
+
elements.append(
|
251
|
+
ConfigElementViewData(
|
252
|
+
elem.name,
|
253
|
+
elem.level,
|
254
|
+
ConfigElementType.MENU
|
255
|
+
if elem.is_menu
|
256
|
+
else ConfigElementType.CONFIG,
|
257
|
+
)
|
258
|
+
)
|
259
|
+
return elements
|
260
|
+
|
261
|
+
def get_variants(self) -> List[VariantViewData]:
|
262
|
+
variants = []
|
263
|
+
|
264
|
+
for variant in self.variant_configs:
|
265
|
+
variants.append(
|
266
|
+
VariantViewData(
|
267
|
+
variant.name,
|
268
|
+
{
|
269
|
+
config_elem.name: config_elem.raw_value
|
270
|
+
for config_elem in variant.config.elements
|
271
|
+
if not config_elem.is_menu
|
272
|
+
},
|
273
|
+
)
|
274
|
+
)
|
275
|
+
return variants
|
276
|
+
|
277
|
+
def _get_variant_name(self, file: Path) -> str:
|
278
|
+
return file.relative_to(self.project_root_dir / "variants").parent.name
|
279
|
+
|
280
|
+
def _search_variant_config_file(self, project_dir: Path) -> List[Path]:
|
281
|
+
"""
|
282
|
+
Finds all files called 'config.txt' in the variants directory
|
283
|
+
and returns a list with their paths.
|
284
|
+
"""
|
285
|
+
return list((project_dir / "variants").glob("**/config.txt"))
|
286
|
+
|
287
|
+
|
288
|
+
class KSPL(Presenter):
|
289
|
+
def __init__(self, event_manager: EventManager, project_dir: Path) -> None:
|
290
|
+
self.event_manager = event_manager
|
291
|
+
self.event_manager.subscribe(KSplEvents.EDIT, self.edit)
|
292
|
+
self.counter = 0
|
293
|
+
self.logger = logger.bind()
|
294
|
+
self.kconfig_data = SPLKConfigData(project_dir)
|
295
|
+
self.view = MainView(
|
296
|
+
self.event_manager,
|
297
|
+
self.kconfig_data.get_elements(),
|
298
|
+
self.kconfig_data.get_variants(),
|
299
|
+
)
|
300
|
+
|
301
|
+
def edit(self) -> None:
|
302
|
+
self.counter += 1
|
303
|
+
self.logger.info(f"Counter: {self.counter}")
|
304
|
+
|
305
|
+
def run(self) -> None:
|
306
|
+
self.view.mainloop()
|
307
|
+
|
308
|
+
|
309
|
+
@dataclass
|
310
|
+
class GuiCommandConfig(DataClassDictMixin):
|
311
|
+
project_dir: Path = field(
|
312
|
+
default=Path(".").absolute(),
|
313
|
+
metadata={
|
314
|
+
"help": "Project root directory. "
|
315
|
+
"Defaults to the current directory if not specified."
|
316
|
+
},
|
317
|
+
)
|
318
|
+
|
319
|
+
@classmethod
|
320
|
+
def from_namespace(cls, namespace: Namespace) -> "GuiCommandConfig":
|
321
|
+
return cls.from_dict(vars(namespace))
|
322
|
+
|
323
|
+
|
324
|
+
class GuiCommand(Command):
|
325
|
+
def __init__(self) -> None:
|
326
|
+
super().__init__("gui", "Start the GUI for SPL configuration.")
|
327
|
+
self.logger = logger.bind()
|
328
|
+
|
329
|
+
@time_it("Build")
|
330
|
+
def run(self, args: Namespace) -> int:
|
331
|
+
self.logger.info(f"Running {self.name} with args {args}")
|
332
|
+
config = GuiCommandConfig.from_namespace(args)
|
333
|
+
event_manager = EventManager()
|
334
|
+
KSPL(event_manager, config.project_dir.absolute()).run()
|
335
|
+
return 0
|
336
|
+
|
337
|
+
def _register_arguments(self, parser: ArgumentParser) -> None:
|
338
|
+
register_arguments_for_config_dataclass(parser, GuiCommandConfig)
|
@@ -0,0 +1,221 @@
|
|
1
|
+
import os
|
2
|
+
import re
|
3
|
+
from contextlib import contextmanager
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from enum import Enum, auto
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any, Generator, List, Optional
|
8
|
+
|
9
|
+
import kconfiglib
|
10
|
+
from kconfiglib import MenuNode
|
11
|
+
|
12
|
+
|
13
|
+
class TriState(Enum):
|
14
|
+
Y = auto()
|
15
|
+
M = auto()
|
16
|
+
N = auto()
|
17
|
+
|
18
|
+
|
19
|
+
class ConfigElementType(Enum):
|
20
|
+
UNKNOWN = auto()
|
21
|
+
BOOL = auto()
|
22
|
+
TRISTATE = auto()
|
23
|
+
STRING = auto()
|
24
|
+
INT = auto()
|
25
|
+
HEX = auto()
|
26
|
+
MENU = auto()
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class ConfigElement:
|
31
|
+
type: ConfigElementType
|
32
|
+
name: str
|
33
|
+
value: Any
|
34
|
+
|
35
|
+
@property
|
36
|
+
def is_menu(self) -> bool:
|
37
|
+
return self.type == ConfigElementType.MENU
|
38
|
+
|
39
|
+
@property
|
40
|
+
def raw_value(self) -> int | str | bool:
|
41
|
+
value = self.value
|
42
|
+
if self.type == ConfigElementType.BOOL:
|
43
|
+
value = self.value == TriState.Y
|
44
|
+
elif self.type == ConfigElementType.TRISTATE:
|
45
|
+
value = str(self.value)
|
46
|
+
elif self.type == ConfigElementType.HEX:
|
47
|
+
value = hex(self.value)
|
48
|
+
return value
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass
|
52
|
+
class EditableConfigElement(ConfigElement):
|
53
|
+
original_value: Any
|
54
|
+
|
55
|
+
#: The level of the menu this element is in. 0 is the top level.
|
56
|
+
level: int = 0
|
57
|
+
#: Is determined when the value is calculated. This is a hidden function call due to property magic.
|
58
|
+
write_to_conf: bool = True
|
59
|
+
|
60
|
+
@property
|
61
|
+
def id(self) -> str:
|
62
|
+
return self.name
|
63
|
+
|
64
|
+
@property
|
65
|
+
def has_been_changed(self) -> bool:
|
66
|
+
return self.original_value != self.value
|
67
|
+
|
68
|
+
|
69
|
+
@dataclass
|
70
|
+
class ConfigurationData:
|
71
|
+
"""Holds the variant configuration data which is relevant for the code generation
|
72
|
+
Requires no variable substitution (this should have been already done)"""
|
73
|
+
|
74
|
+
elements: List[ConfigElement]
|
75
|
+
|
76
|
+
|
77
|
+
@contextmanager
|
78
|
+
def working_directory(some_directory: Path) -> Generator[None, Any, None]:
|
79
|
+
current_directory = Path().absolute()
|
80
|
+
try:
|
81
|
+
os.chdir(some_directory)
|
82
|
+
yield
|
83
|
+
finally:
|
84
|
+
os.chdir(current_directory)
|
85
|
+
|
86
|
+
|
87
|
+
class KConfig:
|
88
|
+
def __init__(
|
89
|
+
self,
|
90
|
+
k_config_model_file: Path,
|
91
|
+
k_config_file: Optional[Path] = None,
|
92
|
+
k_config_root_directory: Optional[Path] = None,
|
93
|
+
):
|
94
|
+
"""
|
95
|
+
:param k_config_model_file: Feature model definition (KConfig format)
|
96
|
+
:param k_config_file: User feature selection configuration file
|
97
|
+
:param k_config_root_directory: all paths for the included configuration paths shall be relative to this folder
|
98
|
+
"""
|
99
|
+
if not k_config_model_file.is_file():
|
100
|
+
raise FileNotFoundError(f"File {k_config_model_file} does not exist.")
|
101
|
+
with working_directory(k_config_root_directory or k_config_model_file.parent):
|
102
|
+
self._config = kconfiglib.Kconfig(k_config_model_file.absolute().as_posix())
|
103
|
+
if k_config_file:
|
104
|
+
if not k_config_file.is_file():
|
105
|
+
raise FileNotFoundError(f"File {k_config_file} does not exist.")
|
106
|
+
self._config.load_config(k_config_file, replace=False)
|
107
|
+
self.elements = self._collect_elements()
|
108
|
+
self.config = self._create_config_data()
|
109
|
+
|
110
|
+
def _create_config_data(self) -> ConfigurationData:
|
111
|
+
"""- creates the ConfigurationData from the KConfig configuration"""
|
112
|
+
elements = self.elements
|
113
|
+
elements_dict = {element.id: element for element in elements}
|
114
|
+
|
115
|
+
# replace text in KConfig with referenced variables (string type only)
|
116
|
+
# KConfig variables get replaced like: ${VARIABLE_NAME}, e.g. ${CONFIG_FOO}
|
117
|
+
for element in elements:
|
118
|
+
if element.type == ConfigElementType.STRING:
|
119
|
+
element.value = re.sub(
|
120
|
+
r"\$\{([A-Za-z0-9_]+)\}",
|
121
|
+
lambda m: str(elements_dict[m.group(1)].value),
|
122
|
+
element.value,
|
123
|
+
)
|
124
|
+
element.value = re.sub(
|
125
|
+
r"\$\{ENV:([A-Za-z0-9_]+)\}",
|
126
|
+
lambda m: str(os.environ.get(m.group(1), "")),
|
127
|
+
element.value,
|
128
|
+
)
|
129
|
+
|
130
|
+
return ConfigurationData(
|
131
|
+
[
|
132
|
+
ConfigElement(elem.type, elem.name, elem.value)
|
133
|
+
for elem in elements
|
134
|
+
if elem.type != ConfigElementType.MENU
|
135
|
+
]
|
136
|
+
)
|
137
|
+
|
138
|
+
def _collect_elements(self) -> List[EditableConfigElement]:
|
139
|
+
elements: List[EditableConfigElement] = []
|
140
|
+
|
141
|
+
def convert_to_element(
|
142
|
+
node: MenuNode, level: int
|
143
|
+
) -> Optional[EditableConfigElement]:
|
144
|
+
# TODO: Symbols like 'choice' and 'comment' shall be ignored.
|
145
|
+
element = None
|
146
|
+
sym = node.item
|
147
|
+
if isinstance(sym, kconfiglib.Symbol):
|
148
|
+
if sym.config_string:
|
149
|
+
val = sym.str_value
|
150
|
+
type = ConfigElementType.STRING
|
151
|
+
if sym.type in [kconfiglib.BOOL, kconfiglib.TRISTATE]:
|
152
|
+
val = getattr(TriState, str(val).upper())
|
153
|
+
type = (
|
154
|
+
ConfigElementType.BOOL
|
155
|
+
if sym.type == kconfiglib.BOOL
|
156
|
+
else ConfigElementType.TRISTATE
|
157
|
+
)
|
158
|
+
elif sym.type == kconfiglib.HEX:
|
159
|
+
val = int(str(val), 16)
|
160
|
+
type = ConfigElementType.HEX
|
161
|
+
elif sym.type == kconfiglib.INT:
|
162
|
+
val = int(val)
|
163
|
+
type = ConfigElementType.INT
|
164
|
+
element = EditableConfigElement(
|
165
|
+
type=type,
|
166
|
+
name=sym.name,
|
167
|
+
value=val,
|
168
|
+
original_value=val,
|
169
|
+
level=level,
|
170
|
+
write_to_conf=sym._write_to_conf,
|
171
|
+
)
|
172
|
+
else:
|
173
|
+
if isinstance(node, kconfiglib.MenuNode):
|
174
|
+
element = EditableConfigElement(
|
175
|
+
type=ConfigElementType.MENU,
|
176
|
+
name=node.prompt[0],
|
177
|
+
value=None,
|
178
|
+
original_value=None,
|
179
|
+
level=level,
|
180
|
+
write_to_conf=False,
|
181
|
+
)
|
182
|
+
return element
|
183
|
+
|
184
|
+
def _shown_full_nodes(node: MenuNode) -> List[MenuNode]:
|
185
|
+
# Returns the list of menu nodes shown in 'menu' (a menu node for a menu)
|
186
|
+
# for full-tree mode. A tricky detail is that invisible items need to be
|
187
|
+
# shown if they have visible children.
|
188
|
+
|
189
|
+
def rec(node: MenuNode) -> List[MenuNode]:
|
190
|
+
res = []
|
191
|
+
|
192
|
+
while node:
|
193
|
+
res.append(node)
|
194
|
+
if node.list and isinstance(node.item, kconfiglib.Symbol):
|
195
|
+
# Nodes from menu created from dependencies
|
196
|
+
res += rec(node.list)
|
197
|
+
node = node.next
|
198
|
+
|
199
|
+
return res
|
200
|
+
|
201
|
+
return rec(node.list)
|
202
|
+
|
203
|
+
def create_elements_tree(
|
204
|
+
node: MenuNode, collected_nodes: List[EditableConfigElement], level: int = 0
|
205
|
+
) -> None:
|
206
|
+
# Updates the tree starting from menu.list, in full-tree mode. To speed
|
207
|
+
# things up, only open menus are updated. The menu-at-a-time logic here is
|
208
|
+
# to deal with invisible items that can show up outside show-all mode (see
|
209
|
+
# _shown_full_nodes()).
|
210
|
+
|
211
|
+
for node in _shown_full_nodes(node):
|
212
|
+
element = convert_to_element(node, level)
|
213
|
+
if element:
|
214
|
+
collected_nodes.append(element)
|
215
|
+
# _shown_full_nodes() includes nodes from menus rooted at symbols, so
|
216
|
+
# we only need to check "real" menus/choices here
|
217
|
+
if node.list and not isinstance(node.item, kconfiglib.Symbol):
|
218
|
+
create_elements_tree(node, collected_nodes, level + 1)
|
219
|
+
|
220
|
+
create_elements_tree(self._config.top_node, elements)
|
221
|
+
return elements
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import sys
|
2
|
+
from argparse import ArgumentParser
|
3
|
+
from sys import argv
|
4
|
+
|
5
|
+
from py_app_dev.core.cmd_line import CommandLineHandlerBuilder
|
6
|
+
from py_app_dev.core.exceptions import UserNotificationException
|
7
|
+
from py_app_dev.core.logging import logger, setup_logger
|
8
|
+
|
9
|
+
from kspl import __version__
|
10
|
+
from kspl.generate import GenerateCommand
|
11
|
+
from kspl.gui import GuiCommand
|
12
|
+
|
13
|
+
|
14
|
+
def do_run() -> None:
|
15
|
+
parser = ArgumentParser(
|
16
|
+
prog="kspl", description="kconfig for SPL", exit_on_error=False
|
17
|
+
)
|
18
|
+
parser.add_argument(
|
19
|
+
"-v", "--version", action="version", version=f"%(prog)s {__version__}"
|
20
|
+
)
|
21
|
+
builder = CommandLineHandlerBuilder(parser)
|
22
|
+
builder.add_commands([GuiCommand(), GenerateCommand()])
|
23
|
+
handler = builder.create()
|
24
|
+
handler.run(argv[1:])
|
25
|
+
|
26
|
+
|
27
|
+
def main() -> int:
|
28
|
+
try:
|
29
|
+
setup_logger()
|
30
|
+
do_run()
|
31
|
+
except UserNotificationException as e:
|
32
|
+
logger.error(f"{e}")
|
33
|
+
return 1
|
34
|
+
return 0
|
35
|
+
|
36
|
+
|
37
|
+
if __name__ == "__main__":
|
38
|
+
sys.exit(main())
|
File without changes
|