proj-flow 0.9.3__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proj_flow/__init__.py +6 -1
- proj_flow/api/arg.py +47 -24
- proj_flow/api/ctx.py +43 -23
- proj_flow/api/env.py +7 -2
- proj_flow/api/makefile.py +1 -1
- proj_flow/api/step.py +3 -5
- proj_flow/base/name_list.py +19 -0
- proj_flow/base/plugins.py +1 -36
- proj_flow/base/registry.py +19 -4
- proj_flow/cli/__init__.py +2 -4
- proj_flow/cli/argument.py +3 -3
- proj_flow/{flow/dependency.py → dependency.py} +1 -1
- proj_flow/ext/cplusplus/__init__.py +10 -0
- proj_flow/ext/cplusplus/cmake/__init__.py +12 -0
- proj_flow/{plugins → ext/cplusplus}/cmake/__version__.py +5 -0
- proj_flow/{plugins → ext/cplusplus}/cmake/context.py +10 -8
- proj_flow/{plugins → ext/cplusplus}/cmake/parser.py +6 -28
- proj_flow/ext/cplusplus/cmake/steps.py +142 -0
- proj_flow/ext/cplusplus/cmake/version.py +35 -0
- proj_flow/{plugins → ext/cplusplus}/conan/__init__.py +7 -3
- proj_flow/{plugins → ext/cplusplus}/conan/_conan.py +8 -3
- proj_flow/ext/github/__init__.py +2 -2
- proj_flow/ext/github/cli.py +2 -11
- proj_flow/{plugins/github.py → ext/github/switches.py} +3 -3
- proj_flow/ext/{markdown_changelist.py → markdown_changelog.py} +2 -1
- proj_flow/ext/python/rtdocs.py +1 -1
- proj_flow/ext/python/version.py +1 -2
- proj_flow/ext/{re_structured_changelist.py → re_structured_changelog.py} +3 -1
- proj_flow/{plugins → ext}/sign/__init__.py +64 -44
- proj_flow/ext/sign/api.py +83 -0
- proj_flow/ext/sign/win32.py +152 -0
- proj_flow/{plugins/store/store_packages.py → ext/store.py} +51 -9
- proj_flow/flow/__init__.py +2 -2
- proj_flow/log/release.py +1 -1
- proj_flow/log/rich_text/markdown.py +1 -1
- proj_flow/log/rich_text/re_structured_text.py +1 -1
- proj_flow/minimal/__init__.py +2 -2
- proj_flow/{plugins → minimal}/base.py +3 -2
- proj_flow/{plugins/commands → minimal}/init.py +44 -11
- proj_flow/minimal/run.py +1 -2
- proj_flow/project/__init__.py +11 -0
- proj_flow/project/api.py +51 -0
- proj_flow/project/cplusplus.py +17 -0
- proj_flow/project/data.py +14 -0
- proj_flow/{flow → project}/interact.py +114 -13
- {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/METADATA +3 -2
- {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/RECORD +50 -55
- proj_flow/flow/init.py +0 -65
- proj_flow/plugins/__init__.py +0 -8
- proj_flow/plugins/cmake/__init__.py +0 -11
- proj_flow/plugins/cmake/build.py +0 -29
- proj_flow/plugins/cmake/config.py +0 -59
- proj_flow/plugins/cmake/pack.py +0 -37
- proj_flow/plugins/cmake/test.py +0 -29
- proj_flow/plugins/commands/__init__.py +0 -12
- proj_flow/plugins/commands/ci/__init__.py +0 -17
- proj_flow/plugins/commands/ci/changelog.py +0 -47
- proj_flow/plugins/commands/ci/matrix.py +0 -46
- proj_flow/plugins/commands/ci/release.py +0 -116
- proj_flow/plugins/sign/win32.py +0 -191
- proj_flow/plugins/store/__init__.py +0 -11
- proj_flow/plugins/store/store_both.py +0 -22
- proj_flow/plugins/store/store_tests.py +0 -21
- {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/WHEEL +0 -0
- {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/entry_points.txt +0 -0
- {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.ext.cplusplus.cmake.steps** defines steps for configuring,
|
|
6
|
+
building and archiving.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Dict, List, cast
|
|
11
|
+
|
|
12
|
+
from proj_flow import api
|
|
13
|
+
from proj_flow.api import env, step
|
|
14
|
+
|
|
15
|
+
from .__version__ import CMAKE_VERSION
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CMakeBase(api.step.Step):
|
|
19
|
+
_name: str
|
|
20
|
+
_runs_after: List[str] = []
|
|
21
|
+
_runs_before: List[str] = []
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self):
|
|
25
|
+
return self._name
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def runs_after(self):
|
|
29
|
+
return self._runs_after
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def runs_before(self):
|
|
33
|
+
return self._runs_before
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self, name: str, runs_after: List[str] = [], runs_before: List[str] = []
|
|
37
|
+
):
|
|
38
|
+
super().__init__()
|
|
39
|
+
self._name = name
|
|
40
|
+
self._runs_after = runs_after
|
|
41
|
+
self._runs_before = runs_before
|
|
42
|
+
|
|
43
|
+
def is_active(self, config: api.env.Config, rt: api.env.Runtime) -> int:
|
|
44
|
+
return os.path.isfile("CMakeLists.txt") and os.path.isfile("CMakePresets.json")
|
|
45
|
+
|
|
46
|
+
def platform_dependencies(self):
|
|
47
|
+
return [f"cmake>={CMAKE_VERSION}"]
|
|
48
|
+
|
|
49
|
+
def dep_with_tool(self, tool: str):
|
|
50
|
+
return [f"cmake>={CMAKE_VERSION}", f"{tool}>={CMAKE_VERSION}"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@step.register
|
|
54
|
+
class CMakeConfig(CMakeBase):
|
|
55
|
+
"""Configures the project using ``preset`` config."""
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
super().__init__(name="CMake")
|
|
59
|
+
|
|
60
|
+
def is_active(self, config: env.Config, rt: env.Runtime) -> int:
|
|
61
|
+
return os.path.isfile("CMakeLists.txt") and os.path.isfile("CMakePresets.json")
|
|
62
|
+
|
|
63
|
+
def directories_to_remove(self, config: env.Config) -> List[str]:
|
|
64
|
+
return [f"build/{config.build_type}"]
|
|
65
|
+
|
|
66
|
+
def run(self, config: env.Config, rt: env.Runtime) -> int:
|
|
67
|
+
cmake_vars = cast(Dict[str, str], rt._cfg.get("cmake", {}).get("vars", {}))
|
|
68
|
+
defines: List[str] = []
|
|
69
|
+
for var in cmake_vars:
|
|
70
|
+
value = cmake_vars[var]
|
|
71
|
+
|
|
72
|
+
is_flag = value.startswith("?")
|
|
73
|
+
if is_flag:
|
|
74
|
+
value = value[1:]
|
|
75
|
+
|
|
76
|
+
if value.startswith("config:"):
|
|
77
|
+
value = value[len("config:")]
|
|
78
|
+
value = config.get_path(value)
|
|
79
|
+
elif value.startswith("runtime:"):
|
|
80
|
+
value = value[len("runtime:")]
|
|
81
|
+
value = getattr(rt, value, None)
|
|
82
|
+
|
|
83
|
+
if is_flag:
|
|
84
|
+
value = "ON" if value else "OFF"
|
|
85
|
+
|
|
86
|
+
defines.append(f"-D{var}={value}")
|
|
87
|
+
|
|
88
|
+
return rt.cmd(
|
|
89
|
+
"cmake",
|
|
90
|
+
"--preset",
|
|
91
|
+
f"{config.preset}-{config.build_generator}",
|
|
92
|
+
*defines,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@step.register()
|
|
97
|
+
class CMakeBuild(CMakeBase):
|
|
98
|
+
"""Builds the project using ``preset`` config."""
|
|
99
|
+
|
|
100
|
+
def __init__(self):
|
|
101
|
+
super().__init__(name="Build", runs_after=["CMake"])
|
|
102
|
+
|
|
103
|
+
def run(self, config: env.Config, rt: env.Runtime) -> int:
|
|
104
|
+
return rt.cmd("cmake", "--build", "--preset", config.preset, "--parallel")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@step.register
|
|
108
|
+
class CMakeTest(CMakeBase):
|
|
109
|
+
"""Runs tests in the project using ``preset`` config."""
|
|
110
|
+
|
|
111
|
+
def __init__(self):
|
|
112
|
+
super().__init__(name="Test", runs_after=["Build"])
|
|
113
|
+
|
|
114
|
+
def platform_dependencies(self):
|
|
115
|
+
return self.dep_with_tool("ctest")
|
|
116
|
+
|
|
117
|
+
def run(self, config: env.Config, rt: env.Runtime) -> int:
|
|
118
|
+
return rt.cmd("ctest", "--preset", config.preset)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@step.register
|
|
122
|
+
class PackStep(CMakeBase):
|
|
123
|
+
"""
|
|
124
|
+
Packs archives and installers from ``cpack_generator`` config, using
|
|
125
|
+
``preset`` config.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
def __init__(self):
|
|
129
|
+
super().__init__(name="Pack", runs_after=["Build"])
|
|
130
|
+
|
|
131
|
+
def platform_dependencies(self):
|
|
132
|
+
return self.dep_with_tool("cpack")
|
|
133
|
+
|
|
134
|
+
def is_active(self, config: env.Config, rt: env.Runtime) -> int:
|
|
135
|
+
return (
|
|
136
|
+
super().is_active(config, rt)
|
|
137
|
+
and len(config.items.get("cpack_generator", [])) > 0
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def run(self, config: env.Config, rt: env.Runtime) -> int:
|
|
141
|
+
generators = ";".join(config.items.get("cpack_generator", []))
|
|
142
|
+
return rt.cmd("cpack", "--preset", config.preset, "-G", generators)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.ext.cplusplus.cmake.version** provides project suite plugin.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from typing import NamedTuple, Optional
|
|
11
|
+
|
|
12
|
+
import toml
|
|
13
|
+
|
|
14
|
+
from proj_flow.api import env, release
|
|
15
|
+
from proj_flow.ext.cplusplus.cmake.parser import get_project
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class QuickProjectInfo(NamedTuple):
|
|
19
|
+
name: Optional[str] = None
|
|
20
|
+
path: Optional[str] = None
|
|
21
|
+
pattern: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@release.project_suites.add
|
|
25
|
+
class ProjectSuite(release.ProjectSuite):
|
|
26
|
+
def get_project(self, rt: env.Runtime) -> Optional[release.Project]:
|
|
27
|
+
project = get_project(rt.root)
|
|
28
|
+
if project is None:
|
|
29
|
+
return None
|
|
30
|
+
return release.Project(
|
|
31
|
+
project.name.value, release.Version(project.version, project.stability)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def get_version_file_path(self, rt: env.Runtime) -> Optional[str]:
|
|
35
|
+
return "CMakeLists.txt"
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
The **proj_flow.
|
|
5
|
+
The **proj_flow.ext.cplusplus.conan** provides the ``"Conan"`` step.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import textwrap
|
|
10
10
|
from typing import List
|
|
11
11
|
|
|
12
|
-
from proj_flow.api import
|
|
12
|
+
from proj_flow.api import env, step
|
|
13
|
+
from proj_flow.project import cplusplus
|
|
13
14
|
|
|
14
15
|
from ._conan import conan_api
|
|
15
16
|
|
|
@@ -23,6 +24,7 @@ class ConanConfig:
|
|
|
23
24
|
"""Configures the project for ``preset`` config using ``build_type`` config."""
|
|
24
25
|
|
|
25
26
|
name = "Conan"
|
|
27
|
+
runs_before = ["CMake"]
|
|
26
28
|
|
|
27
29
|
def platform_dependencies(self):
|
|
28
30
|
return ["conan"]
|
|
@@ -63,4 +65,6 @@ class ConanConfig:
|
|
|
63
65
|
return 0
|
|
64
66
|
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
cplusplus.project.register_switch(
|
|
69
|
+
"with_conan", "Use Conan for dependency manager", True
|
|
70
|
+
)
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
The **proj_flow.
|
|
5
|
+
The **proj_flow.ext.cplusplus.conan._conan** adds support for both Conan v1
|
|
6
|
+
and v2.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import shutil
|
|
10
|
+
import subprocess
|
|
9
11
|
from abc import ABC, abstractmethod
|
|
10
12
|
from typing import Callable, List, cast
|
|
11
13
|
|
|
12
14
|
from proj_flow.api.env import Config, Runtime
|
|
13
15
|
from proj_flow.base import cmd
|
|
14
|
-
from proj_flow.
|
|
16
|
+
from proj_flow.dependency import VER_REGEX
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class conan(ABC):
|
|
@@ -119,7 +121,10 @@ def _conan_version():
|
|
|
119
121
|
if found is None:
|
|
120
122
|
return 1
|
|
121
123
|
|
|
122
|
-
proc =
|
|
124
|
+
proc = cast(
|
|
125
|
+
subprocess.CompletedProcess[str],
|
|
126
|
+
cmd.run(found, "--version", capture_output=True),
|
|
127
|
+
)
|
|
123
128
|
if proc.returncode != 0:
|
|
124
129
|
return 1
|
|
125
130
|
m = VER_REGEX.search(proc.stdout)
|
proj_flow/ext/github/__init__.py
CHANGED
proj_flow/ext/github/cli.py
CHANGED
|
@@ -16,22 +16,13 @@ import typing
|
|
|
16
16
|
|
|
17
17
|
from proj_flow import log
|
|
18
18
|
from proj_flow.api import arg, env
|
|
19
|
+
from proj_flow.base.name_list import name_list
|
|
19
20
|
from proj_flow.flow.configs import Configs
|
|
20
21
|
from proj_flow.log import commit, hosting, rich_text
|
|
21
22
|
|
|
22
23
|
FORCED_LEVEL_CHOICES = list(commit.FORCED_LEVEL.keys())
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def _name_list(names: typing.List[str]) -> str:
|
|
26
|
-
if len(names) == 0:
|
|
27
|
-
return ""
|
|
28
|
-
|
|
29
|
-
prefix = ", ".join(names[:-1])
|
|
30
|
-
if prefix:
|
|
31
|
-
prefix += " and "
|
|
32
|
-
return f"{prefix}{names[-1]}"
|
|
33
|
-
|
|
34
|
-
|
|
35
26
|
@arg.command("github")
|
|
36
27
|
def github():
|
|
37
28
|
"""Interact with GitHub workflows and releases"""
|
|
@@ -81,7 +72,7 @@ def release(
|
|
|
81
72
|
typing.Optional[str],
|
|
82
73
|
arg.Argument(
|
|
83
74
|
help="Ignore the version change from changelog and instead use this value. "
|
|
84
|
-
f"Allowed values are: {
|
|
75
|
+
f"Allowed values are: {name_list(FORCED_LEVEL_CHOICES)}",
|
|
85
76
|
meta="level",
|
|
86
77
|
choices=FORCED_LEVEL_CHOICES,
|
|
87
78
|
),
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Copyright (c) 2025 Marcin Zdun
|
|
2
2
|
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
3
|
"""
|
|
4
|
-
The **proj_flow.
|
|
4
|
+
The **proj_flow.ext.github.switches** provides GitHub-related switches for new
|
|
5
5
|
projects.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from proj_flow.api import ctx
|
|
9
9
|
|
|
10
|
-
ctx.
|
|
11
|
-
ctx.
|
|
10
|
+
ctx.register_common_switch("with_github_actions", "Use Github Actions", True)
|
|
11
|
+
ctx.register_common_switch(
|
|
12
12
|
"with_github_social", "Use Github ISSUE_TEMPLATE, CONTRIBUTING.md, etc.", True
|
|
13
13
|
)
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
The **proj_flow.ext.
|
|
5
|
+
The **proj_flow.ext.markdown_changelog** registers a
|
|
6
|
+
:class:`api.ChangelogGenerator` plugin implemented for markdown changelogs.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from proj_flow.log.rich_text.api import changelog_generators
|
proj_flow/ext/python/rtdocs.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
The **proj_flow.ext.python.
|
|
5
|
+
The **proj_flow.ext.python.rtdocs** defines RTDocs step (`"RTD"`), which uses
|
|
6
6
|
.readthedocs.yaml to build the HTML documentation.
|
|
7
7
|
"""
|
|
8
8
|
|
proj_flow/ext/python/version.py
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
The **proj_flow.ext.
|
|
5
|
+
The **proj_flow.ext.re_structured_changelog** registers a
|
|
6
|
+
:class:`api.ChangelogGenerator` plugin implemented for reStructuredText
|
|
7
|
+
changelogs.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from proj_flow.log.rich_text.api import changelog_generators
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
The **proj_flow.
|
|
5
|
+
The **proj_flow.ext.sign** provides the ``"Sign"`` and ``"SignPackages"``
|
|
6
6
|
steps.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -14,23 +14,7 @@ from typing import List, cast
|
|
|
14
14
|
|
|
15
15
|
from proj_flow.api import env, init, step
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
from . import win32
|
|
19
|
-
|
|
20
|
-
else:
|
|
21
|
-
|
|
22
|
-
class win32:
|
|
23
|
-
@staticmethod
|
|
24
|
-
def is_active(*args):
|
|
25
|
-
return False
|
|
26
|
-
|
|
27
|
-
@staticmethod
|
|
28
|
-
def sign(*args):
|
|
29
|
-
return 0
|
|
30
|
-
|
|
31
|
-
@staticmethod
|
|
32
|
-
def is_pe_exec(arg):
|
|
33
|
-
return False
|
|
17
|
+
from . import api, win32
|
|
34
18
|
|
|
35
19
|
|
|
36
20
|
def should_exclude(filename: str, exclude: List[str], config_os: str):
|
|
@@ -44,34 +28,69 @@ def should_exclude(filename: str, exclude: List[str], config_os: str):
|
|
|
44
28
|
|
|
45
29
|
|
|
46
30
|
class SignBase(step.Step):
|
|
31
|
+
_name: str
|
|
32
|
+
_runs_after: List[str] = []
|
|
33
|
+
_runs_before: List[str] = []
|
|
34
|
+
|
|
35
|
+
_active_tools: List[api.SigningTool] = []
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def name(self):
|
|
39
|
+
return self._name
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def runs_after(self):
|
|
43
|
+
return self._runs_after
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def runs_before(self):
|
|
47
|
+
return self._runs_before
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self, name: str, runs_after: List[str] = [], runs_before: List[str] = []
|
|
51
|
+
):
|
|
52
|
+
super().__init__()
|
|
53
|
+
self._name = name
|
|
54
|
+
self._runs_after = runs_after
|
|
55
|
+
self._runs_before = runs_before
|
|
56
|
+
|
|
47
57
|
def is_active(self, config: env.Config, rt: env.Runtime) -> int:
|
|
48
|
-
|
|
58
|
+
self._active_tools = [
|
|
59
|
+
tool for tool in api.signing_tool.get() if tool.is_active(config, rt)
|
|
60
|
+
]
|
|
61
|
+
return len(self._active_tools) > 0
|
|
49
62
|
|
|
50
63
|
@abstractmethod
|
|
51
|
-
def get_files(
|
|
64
|
+
def get_files(
|
|
65
|
+
self, tool: api.SigningTool, config: env.Config, rt: env.Runtime
|
|
66
|
+
) -> List[str]: ...
|
|
52
67
|
|
|
53
68
|
def run(self, config: env.Config, rt: env.Runtime) -> int:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
for tool in self._active_tools:
|
|
70
|
+
files = [
|
|
71
|
+
file.replace(os.sep, "/") for file in self.get_files(tool, config, rt)
|
|
72
|
+
]
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
if len(files) == 0:
|
|
75
|
+
continue
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
result = tool.sign(config, rt, files)
|
|
78
|
+
if result:
|
|
79
|
+
return result
|
|
62
80
|
|
|
63
|
-
return
|
|
81
|
+
return 0
|
|
64
82
|
|
|
65
83
|
|
|
66
84
|
@step.register
|
|
67
85
|
class SignFiles(SignBase):
|
|
68
86
|
"""*(Windows)* Signs executable files in build directory"""
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
runs_before = ["Pack"]
|
|
88
|
+
def __init__(self):
|
|
89
|
+
super().__init__(name="Sign", runs_after=["Build"], runs_before=["Pack"])
|
|
73
90
|
|
|
74
|
-
def get_files(
|
|
91
|
+
def get_files(
|
|
92
|
+
self, tool: api.SigningTool, config: env.Config, rt: env.Runtime
|
|
93
|
+
) -> List[str]:
|
|
75
94
|
cfg = cast(dict, rt._cfg.get("sign", {}))
|
|
76
95
|
roots = cfg.get("directories", ["bin", "lib", "libexec", "share"])
|
|
77
96
|
exclude = cfg.get("exclude", ["*-test"])
|
|
@@ -85,10 +104,8 @@ class SignFiles(SignBase):
|
|
|
85
104
|
continue
|
|
86
105
|
|
|
87
106
|
full_path = os.path.join(curr_dir, filename)
|
|
88
|
-
if
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
result.append(full_path)
|
|
107
|
+
if tool.is_executable(full_path, as_package=False):
|
|
108
|
+
result.append(full_path)
|
|
92
109
|
return result
|
|
93
110
|
|
|
94
111
|
|
|
@@ -96,26 +113,29 @@ class SignFiles(SignBase):
|
|
|
96
113
|
class SignMsi(SignBase):
|
|
97
114
|
"""*(Windows)* Signs MSI installers in build directory"""
|
|
98
115
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
116
|
+
def __init__(self):
|
|
117
|
+
super().__init__(
|
|
118
|
+
name="SignPackages",
|
|
119
|
+
runs_after=["Pack"],
|
|
120
|
+
runs_before=["StorePackages", "Store"],
|
|
121
|
+
)
|
|
102
122
|
|
|
103
123
|
def is_active(self, config: env.Config, rt: env.Runtime) -> int:
|
|
104
124
|
return super().is_active(config, rt) and "WIX" in config.items.get(
|
|
105
125
|
"cpack_generator", []
|
|
106
126
|
)
|
|
107
127
|
|
|
108
|
-
def get_files(
|
|
128
|
+
def get_files(
|
|
129
|
+
self, tool: api.SigningTool, config: env.Config, rt: env.Runtime
|
|
130
|
+
) -> List[str]:
|
|
109
131
|
result: List[str] = []
|
|
110
132
|
pkg_dir = os.path.join(config.build_dir, "packages")
|
|
111
133
|
for curr_dir, dirnames, filenames in os.walk(pkg_dir):
|
|
112
134
|
dirnames[:] = []
|
|
113
135
|
for filename in filenames:
|
|
114
|
-
|
|
115
|
-
if
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
result.append(os.path.join(curr_dir, filename))
|
|
136
|
+
full_path = os.path.join(curr_dir, filename)
|
|
137
|
+
if tool.is_executable(full_path, as_package=False):
|
|
138
|
+
result.append(full_path)
|
|
119
139
|
|
|
120
140
|
return result
|
|
121
141
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.ext.sign.api** defines an extension point for per-platform
|
|
6
|
+
sign tools.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import List, NamedTuple, Optional
|
|
14
|
+
|
|
15
|
+
from proj_flow import base
|
|
16
|
+
from proj_flow.api import env
|
|
17
|
+
|
|
18
|
+
ENV_KEY = "SIGN_TOKEN"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Key(NamedTuple):
|
|
22
|
+
token: str
|
|
23
|
+
secret: bytes
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_key_from_contents(key: str, rt: env.Runtime):
|
|
27
|
+
try:
|
|
28
|
+
obj = json.loads(key)
|
|
29
|
+
except json.decoder.JSONDecodeError:
|
|
30
|
+
rt.message("sign: the signature is not a valid JSON document")
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
if not isinstance(obj, dict):
|
|
34
|
+
rt.message("sign: the signature is missing required fields")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
token = obj.get("token")
|
|
38
|
+
secret = obj.get("secret")
|
|
39
|
+
if not isinstance(token, str) or not isinstance(secret, str):
|
|
40
|
+
rt.message("sign: the signature is missing required fields")
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
return Key(
|
|
44
|
+
base64.b64decode(token).decode("UTF-8"),
|
|
45
|
+
base64.b64decode(secret),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_key(rt: env.Runtime) -> Optional[Key]:
|
|
50
|
+
rt.message(f"sign: trying ${ENV_KEY}")
|
|
51
|
+
env = os.environ.get(ENV_KEY)
|
|
52
|
+
if env:
|
|
53
|
+
key = _get_key_from_contents(env, rt)
|
|
54
|
+
if key is not None:
|
|
55
|
+
return key
|
|
56
|
+
local_signature = os.path.join(".", "signature.key")
|
|
57
|
+
home_signature = os.path.join(os.path.expanduser("~"), "signature.key")
|
|
58
|
+
for filename in [local_signature, home_signature]:
|
|
59
|
+
rt.message(f"sign: trying {filename}")
|
|
60
|
+
if os.path.isfile(filename):
|
|
61
|
+
with open(filename, encoding="UTF-8") as file:
|
|
62
|
+
result = file.read().strip()
|
|
63
|
+
key = _get_key_from_contents(result, rt)
|
|
64
|
+
if key is not None:
|
|
65
|
+
return key
|
|
66
|
+
|
|
67
|
+
rt.message("sign: no key set up")
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SigningTool(ABC):
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def is_active(self, config: env.Config, rt: env.Runtime): ...
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def sign(self, config: env.Config, rt: env.Runtime, files: List[str]): ...
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def is_executable(self, filename: str, as_package: bool): ...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
signing_tool = base.registry.Registry[SigningTool]("SigningTool")
|