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.
Files changed (66) hide show
  1. proj_flow/__init__.py +6 -1
  2. proj_flow/api/arg.py +47 -24
  3. proj_flow/api/ctx.py +43 -23
  4. proj_flow/api/env.py +7 -2
  5. proj_flow/api/makefile.py +1 -1
  6. proj_flow/api/step.py +3 -5
  7. proj_flow/base/name_list.py +19 -0
  8. proj_flow/base/plugins.py +1 -36
  9. proj_flow/base/registry.py +19 -4
  10. proj_flow/cli/__init__.py +2 -4
  11. proj_flow/cli/argument.py +3 -3
  12. proj_flow/{flow/dependency.py → dependency.py} +1 -1
  13. proj_flow/ext/cplusplus/__init__.py +10 -0
  14. proj_flow/ext/cplusplus/cmake/__init__.py +12 -0
  15. proj_flow/{plugins → ext/cplusplus}/cmake/__version__.py +5 -0
  16. proj_flow/{plugins → ext/cplusplus}/cmake/context.py +10 -8
  17. proj_flow/{plugins → ext/cplusplus}/cmake/parser.py +6 -28
  18. proj_flow/ext/cplusplus/cmake/steps.py +142 -0
  19. proj_flow/ext/cplusplus/cmake/version.py +35 -0
  20. proj_flow/{plugins → ext/cplusplus}/conan/__init__.py +7 -3
  21. proj_flow/{plugins → ext/cplusplus}/conan/_conan.py +8 -3
  22. proj_flow/ext/github/__init__.py +2 -2
  23. proj_flow/ext/github/cli.py +2 -11
  24. proj_flow/{plugins/github.py → ext/github/switches.py} +3 -3
  25. proj_flow/ext/{markdown_changelist.py → markdown_changelog.py} +2 -1
  26. proj_flow/ext/python/rtdocs.py +1 -1
  27. proj_flow/ext/python/version.py +1 -2
  28. proj_flow/ext/{re_structured_changelist.py → re_structured_changelog.py} +3 -1
  29. proj_flow/{plugins → ext}/sign/__init__.py +64 -44
  30. proj_flow/ext/sign/api.py +83 -0
  31. proj_flow/ext/sign/win32.py +152 -0
  32. proj_flow/{plugins/store/store_packages.py → ext/store.py} +51 -9
  33. proj_flow/flow/__init__.py +2 -2
  34. proj_flow/log/release.py +1 -1
  35. proj_flow/log/rich_text/markdown.py +1 -1
  36. proj_flow/log/rich_text/re_structured_text.py +1 -1
  37. proj_flow/minimal/__init__.py +2 -2
  38. proj_flow/{plugins → minimal}/base.py +3 -2
  39. proj_flow/{plugins/commands → minimal}/init.py +44 -11
  40. proj_flow/minimal/run.py +1 -2
  41. proj_flow/project/__init__.py +11 -0
  42. proj_flow/project/api.py +51 -0
  43. proj_flow/project/cplusplus.py +17 -0
  44. proj_flow/project/data.py +14 -0
  45. proj_flow/{flow → project}/interact.py +114 -13
  46. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/METADATA +3 -2
  47. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/RECORD +50 -55
  48. proj_flow/flow/init.py +0 -65
  49. proj_flow/plugins/__init__.py +0 -8
  50. proj_flow/plugins/cmake/__init__.py +0 -11
  51. proj_flow/plugins/cmake/build.py +0 -29
  52. proj_flow/plugins/cmake/config.py +0 -59
  53. proj_flow/plugins/cmake/pack.py +0 -37
  54. proj_flow/plugins/cmake/test.py +0 -29
  55. proj_flow/plugins/commands/__init__.py +0 -12
  56. proj_flow/plugins/commands/ci/__init__.py +0 -17
  57. proj_flow/plugins/commands/ci/changelog.py +0 -47
  58. proj_flow/plugins/commands/ci/matrix.py +0 -46
  59. proj_flow/plugins/commands/ci/release.py +0 -116
  60. proj_flow/plugins/sign/win32.py +0 -191
  61. proj_flow/plugins/store/__init__.py +0 -11
  62. proj_flow/plugins/store/store_both.py +0 -22
  63. proj_flow/plugins/store/store_tests.py +0 -21
  64. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/WHEEL +0 -0
  65. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/entry_points.txt +0 -0
  66. {proj_flow-0.9.3.dist-info → proj_flow-0.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,152 @@
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.win32** provides code signing with SignTool
6
+ from Windows SDKs.
7
+ """
8
+
9
+ import base64
10
+ import json
11
+ import os
12
+ import platform
13
+ import struct
14
+ import subprocess
15
+ import sys
16
+ from typing import Iterable, List, Optional, Tuple
17
+
18
+ from proj_flow.api.env import Config, Msg, Runtime
19
+
20
+ from .api import ENV_KEY, SigningTool, get_key, signing_tool
21
+
22
+ if sys.platform == "win32":
23
+ import winreg
24
+
25
+ @signing_tool.add
26
+ class Win32SigningTool(SigningTool):
27
+ def is_active(self, config: Config, rt: Runtime):
28
+ return _is_active(config.os, rt)
29
+
30
+ def sign(self, config: Config, rt: Runtime, files: List[str]):
31
+ rt.print("signtool", *(os.path.basename(file) for file in files))
32
+ return _sign(files, rt)
33
+
34
+ def is_signable(self, filename: str, as_package: bool):
35
+ if as_package:
36
+ _, ext = os.path.splitext(filename)
37
+ return ext.lower() == ".msi"
38
+ return _is_pe_exec(filename)
39
+
40
+ Version = Tuple[int, int, int]
41
+
42
+ machine = {"ARM64": "arm64", "AMD64": "x64", "X86": "x86"}.get(
43
+ platform.machine(), "x86"
44
+ )
45
+
46
+ def _find_sign_tool(rt: Runtime) -> Optional[str]:
47
+ with winreg.OpenKeyEx( # type: ignore
48
+ winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows Kits\Installed Roots" # type: ignore
49
+ ) as kits:
50
+ try:
51
+ kits_root = winreg.QueryValueEx(kits, "KitsRoot10")[0] # type: ignore
52
+ except FileNotFoundError:
53
+ rt.message("sign/win32: No KitsRoot10 value")
54
+ return None
55
+
56
+ versions: List[Tuple[Version, str]] = []
57
+ try:
58
+ index = 0
59
+ while True:
60
+ ver_str = winreg.EnumKey(kits, index) # type: ignore
61
+ ver = tuple(int(chunk) for chunk in ver_str.split("."))
62
+ index += 1
63
+ versions.append((ver, ver_str)) # type: ignore
64
+ except OSError:
65
+ pass
66
+ versions.sort()
67
+ versions.reverse()
68
+ rt.message(
69
+ "sign/win32: Regarding versions:",
70
+ ", ".join(version[1] for version in versions),
71
+ )
72
+ for _, version in versions:
73
+ # C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe
74
+ sign_tool = os.path.join(kits_root, "bin", version, machine, "signtool.exe")
75
+ if os.path.isfile(sign_tool):
76
+ rt.message("sign/win32: using:", sign_tool)
77
+ return sign_tool
78
+ return None
79
+
80
+ def _is_active(os_name: str, rt: Runtime):
81
+ if os_name != "windows":
82
+ return False
83
+ key = get_key(rt)
84
+ return (
85
+ key is not None
86
+ and key.token is not None
87
+ and key.secret is not None
88
+ and _find_sign_tool(rt) is not None
89
+ )
90
+
91
+ _IMAGE_DOS_HEADER = "HHHHHHHHHHHHHH8sHH20sI"
92
+ _IMAGE_NT_HEADERS_Signature = "H"
93
+ _IMAGE_DOS_HEADER_size = struct.calcsize(_IMAGE_DOS_HEADER)
94
+ _IMAGE_NT_HEADERS_Signature_size = struct.calcsize(_IMAGE_NT_HEADERS_Signature)
95
+ _MZ = 23117
96
+ _PE = 17744
97
+
98
+ def _is_pe_exec(path: str):
99
+ with open(path, "rb") as exe:
100
+ mz_header = exe.read(_IMAGE_DOS_HEADER_size)
101
+ dos_header = struct.unpack(_IMAGE_DOS_HEADER, mz_header)
102
+ if dos_header[0] != _MZ:
103
+ return False
104
+
105
+ PE_offset = dos_header[-1]
106
+ if PE_offset < _IMAGE_DOS_HEADER_size:
107
+ return False
108
+
109
+ if PE_offset > _IMAGE_DOS_HEADER_size:
110
+ exe.read(PE_offset - _IMAGE_DOS_HEADER_size)
111
+
112
+ pe_header = exe.read(_IMAGE_NT_HEADERS_Signature_size)
113
+ signature = struct.unpack(_IMAGE_NT_HEADERS_Signature, pe_header)[0]
114
+ return signature == _PE
115
+
116
+ def _sign(files: Iterable[str], rt: Runtime):
117
+ key = get_key(rt)
118
+
119
+ if key is None or key.token is None or key.secret is None:
120
+ rt.fatal("sign: the key is missing")
121
+
122
+ sign_tool = _find_sign_tool(rt)
123
+ if sign_tool is None:
124
+ rt.message("proj-flow: sign: signtool.exe not found", level=Msg.ALWAYS)
125
+ return 0
126
+
127
+ with open("temp.pfx", "wb") as pfx:
128
+ pfx.write(key.secret)
129
+
130
+ args = [
131
+ sign_tool,
132
+ "sign",
133
+ "/f",
134
+ "temp.pfx",
135
+ "/p",
136
+ key.token,
137
+ "/tr",
138
+ "http://timestamp.digicert.com",
139
+ "/fd",
140
+ "sha256",
141
+ "/td",
142
+ "sha256",
143
+ *files,
144
+ ]
145
+
146
+ result = 1
147
+ try:
148
+ result = subprocess.run(args, shell=False).returncode
149
+ finally:
150
+ os.remove("temp.pfx")
151
+
152
+ return result
@@ -2,18 +2,17 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.plugins.store** provides ``"StorePackages"`` step.
5
+ The **proj_flow.ext.store** provides ``"Store"``, ``"StoreTests"`` and
6
+ ``"StorePackages"`` steps.
6
7
  """
7
8
 
8
9
  import os
9
10
  import shutil
10
11
  from typing import List, cast
11
12
 
12
- from proj_flow.api import env, step
13
+ from proj_flow.api import env, release, step
13
14
  from proj_flow.base.uname import uname
14
15
 
15
- from ..cmake.parser import get_project
16
-
17
16
  _system, _version, _arch = uname()
18
17
  _version = "" if _version is None else f"-{_version}"
19
18
  _project_pkg = None
@@ -26,12 +25,24 @@ def _package_name(config: env.Config, pkg: str, group: str):
26
25
  return f"{pkg}-{_system}{_version}-{_arch}{debug}{suffix}"
27
26
 
28
27
 
28
+ def _get_project(rt: env.Runtime):
29
+ def wrap(suite: release.ProjectSuite):
30
+ return suite.get_project(rt)
31
+
32
+ return wrap
33
+
34
+
29
35
  @step.register
30
- class StorePackages:
36
+ class StorePackages(step.Step):
31
37
  """Stores archives and installers build for ``preset`` config value."""
32
38
 
33
- name = "StorePackages"
34
- runs_after = ["Pack"]
39
+ @property
40
+ def name(self):
41
+ return "StorePackages"
42
+
43
+ @property
44
+ def runs_after(self):
45
+ return ["Pack"]
35
46
 
36
47
  def run(self, config: env.Config, rt: env.Runtime) -> int:
37
48
  if not rt.dry_run:
@@ -41,10 +52,10 @@ class StorePackages:
41
52
 
42
53
  global _project_pkg
43
54
  if _project_pkg is None:
44
- project = get_project("")
55
+ _, project = release.project_suites.find(_get_project(rt))
45
56
  if project is None:
46
57
  rt.fatal(f"Cannot get project information from {rt.root}")
47
- _project_pkg = project.pkg
58
+ _project_pkg = project.archive_name
48
59
 
49
60
  main_group = cast(str, rt._cfg.get("package", {}).get("main-group"))
50
61
  if main_group is not None and not rt.dry_run:
@@ -77,3 +88,34 @@ class StorePackages:
77
88
  "build/artifacts/packages",
78
89
  f"^{_project_pkg}-.*$",
79
90
  )
91
+
92
+
93
+ @step.register
94
+ class StoreTests(step.Step):
95
+ """Stores test results gathered during tests for ``preset`` config value."""
96
+
97
+ @property
98
+ def name(self):
99
+ return "StoreTests"
100
+
101
+ @property
102
+ def runs_after(self):
103
+ return ["Test"]
104
+
105
+ def run(self, config: env.Config, rt: env.Runtime) -> int:
106
+ return rt.cp(
107
+ f"build/{config.preset}/test-results", "build/artifacts/test-results"
108
+ )
109
+
110
+
111
+ @step.register
112
+ class StoreBoth(step.SerialStep):
113
+ """Stores all artifacts created for ``preset`` config value."""
114
+
115
+ @property
116
+ def name(self):
117
+ return "Store"
118
+
119
+ def __init__(self):
120
+ super().__init__()
121
+ self.children = [StoreTests(), StorePackages()]
@@ -6,6 +6,6 @@ The **proj_flow.flow** contains the inner workings of various *Project Flow*
6
6
  components.
7
7
  """
8
8
 
9
- from . import configs, dependency, init, interact, layer, steps
9
+ from . import configs, layer, steps
10
10
 
11
- __all__ = ["configs", "dependency", "init", "interact", "layer", "steps"]
11
+ __all__ = ["configs", "layer", "steps"]
proj_flow/log/release.py CHANGED
@@ -134,4 +134,4 @@ def add_release(
134
134
  if draft_url:
135
135
  rt.message("Visit draft at", draft_url, level=env.Msg.ALWAYS)
136
136
 
137
- return setup.curr_tag
137
+ return setup.curr_tag
@@ -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.log.rich.markdown** provides details of making Markdown
5
+ The **proj_flow.log.rich_text.markdown** provides details of making Markdown
6
6
  changelogs.
7
7
  """
8
8
 
@@ -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.log.rich.re_structured_text** provides details of making
5
+ The **proj_flow.log.rich_text.re_structured_text** provides details of making
6
6
  reStructuredText changelogs.
7
7
  """
8
8
 
@@ -6,6 +6,6 @@ The **proj_flow.minimal** defines minimal extension package: ``bootstrap``
6
6
  and ``run`` commands, with basic set of steps.
7
7
  """
8
8
 
9
- from . import bootstrap, list, run, system
9
+ from . import bootstrap, init, list, run, system
10
10
 
11
- __all__ = ["bootstrap", "list", "run", "system"]
11
+ __all__ = ["bootstrap", "init", "list", "run", "system"]
@@ -2,8 +2,8 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.plugins.base** provides basic initialization setup for new
6
- projects.
5
+ The **proj_flow.minimal.base** provides basic initialization setup for all
6
+ new projects.
7
7
  """
8
8
 
9
9
  from proj_flow import __version__, api
@@ -26,5 +26,6 @@ api.init.register_init_step(GitInit())
26
26
  api.ctx.register_init_setting(
27
27
  api.ctx.Setting("__flow_version__", value=__version__),
28
28
  api.ctx.Setting("${", value="${"),
29
+ project=None,
29
30
  is_hidden=True,
30
31
  )
@@ -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.plugins.commands.init** implements ``proj-flow init`` command.
5
+ The **proj_flow.minimal.init** implements ``proj-flow init`` command.
6
6
  """
7
7
 
8
8
  import json
@@ -10,12 +10,36 @@ import os
10
10
  import sys
11
11
  from typing import Annotated, Optional
12
12
 
13
- from proj_flow import flow
13
+ import yaml
14
+
15
+ from proj_flow import dependency, flow
14
16
  from proj_flow.api import arg, ctx, env, init
17
+ from proj_flow.base.name_list import name_list
18
+ from proj_flow.project import api, interact
19
+
20
+
21
+ def _project_types():
22
+ return list(map(lambda proj: proj.id, api.project_type.get()))
23
+
24
+
25
+ def _project_help():
26
+ return (
27
+ "Type of project to create. "
28
+ f"Allowed values are: {name_list(_project_types())}"
29
+ )
15
30
 
16
31
 
17
32
  @arg.command("init")
18
33
  def main(
34
+ project: Annotated[
35
+ str,
36
+ arg.Argument(
37
+ help=_project_help,
38
+ meta="project",
39
+ pos=True,
40
+ choices=_project_types,
41
+ ),
42
+ ],
19
43
  path: Annotated[
20
44
  Optional[str],
21
45
  arg.Argument(
@@ -38,26 +62,35 @@ def main(
38
62
  ):
39
63
  """Initialize new project"""
40
64
 
65
+ try:
66
+ current_project = api.get_project_type(project)
67
+ except api.ProjectNotFound:
68
+ print(f"proj-flow init: error: project type `{project}` is not known")
69
+ return 1
70
+
41
71
  if path is not None:
42
72
  os.makedirs(path, exist_ok=True)
43
73
  os.chdir(path)
44
74
 
45
- errors = flow.dependency.verify(flow.dependency.gather(init.__steps))
75
+ errors = dependency.verify(dependency.gather(init.__steps))
46
76
  if len(errors) > 0:
47
77
  if not rt.silent:
48
78
  for error in errors:
49
79
  print(f"proj-flow: {error}", file=sys.stderr)
50
80
  return 1
51
81
 
52
- context = flow.init.fixup(
53
- flow.init.all_default() if non_interactive else flow.interact.prompt()
54
- )
82
+ context = current_project.get_context(not non_interactive)
55
83
  if not non_interactive and not rt.silent:
56
84
  print()
57
85
 
58
- if save_context:
59
- with open(".context.json", "w", encoding="UTF-8") as jsonf:
60
- json.dump(context, jsonf, ensure_ascii=False, indent=4)
86
+ if save_context and rt.verbose:
87
+ lines = yaml.dump(context, indent=4).rstrip().split("\n")
88
+ for line in lines:
89
+ rt.message("[CONTEXT]", line)
90
+
91
+ if save_context and not rt.dry_run:
92
+ with open(".context.yaml", "w", encoding="UTF-8") as jsonf:
93
+ yaml.dump(context, jsonf, indent=4)
61
94
 
62
95
  flow.layer.copy_license(rt, context)
63
96
  if not rt.silent:
@@ -67,9 +100,9 @@ def main(
67
100
  for fs_layer in layers:
68
101
  fs_layer.run(rt, context)
69
102
 
70
- if save_context:
103
+ if save_context and not rt.dry_run:
71
104
  with open(".gitignore", "ab") as ignoref:
72
- ignoref.write("\n/.context.json\n".encode("UTF-8"))
105
+ ignoref.write("\n/.context.yaml\n".encode("UTF-8"))
73
106
 
74
107
  for step in init.__steps:
75
108
  step.postprocess(rt, context)
proj_flow/minimal/run.py CHANGED
@@ -11,9 +11,8 @@ import sys
11
11
  from contextlib import contextmanager
12
12
  from typing import Annotated, List, Optional, Set, cast
13
13
 
14
- from proj_flow import api
14
+ from proj_flow import api, dependency
15
15
  from proj_flow.base import matrix
16
- from proj_flow.flow import dependency
17
16
  from proj_flow.flow.configs import Configs
18
17
 
19
18
 
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.project** contains the inner workings of ``proj-flow init``
6
+ command.
7
+ """
8
+
9
+ from . import api, cplusplus, data, interact
10
+
11
+ __all__ = ["api", "cplusplus", "data", "interact"]
@@ -0,0 +1,51 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.project.api** defines an extension point for project suites.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any, List, NamedTuple, Optional
10
+
11
+ from proj_flow import base
12
+ from proj_flow.api import ctx
13
+ from proj_flow.project import interact
14
+
15
+
16
+ class ProjectType(ABC):
17
+ name: str
18
+ id: str
19
+
20
+ def __init__(self, name: str, id: str):
21
+ self.name = name
22
+ self.id = id
23
+
24
+ def register_switch(self, key: str, prompt: str, enabled: bool):
25
+ ctx.register_switch(key, prompt, enabled, self.id)
26
+
27
+ def register_internal(self, key: str, value: Any):
28
+ ctx.register_internal(key, value)
29
+
30
+ def register_init_setting(self, *settings: ctx.Setting, is_hidden=False):
31
+ ctx.register_init_setting(*settings, is_hidden=is_hidden, project=self.id)
32
+
33
+ def get_context(self, interactive: bool):
34
+ return interact.get_context(interactive, self.id)
35
+
36
+
37
+ project_type = base.registry.Registry[ProjectType]("ProjectType")
38
+
39
+
40
+ class ProjectNotFound(Exception):
41
+ name: str
42
+
43
+ def __init__(self, name: str):
44
+ self.name = name
45
+
46
+
47
+ def get_project_type(id: str):
48
+ result, _ = project_type.find(lambda proj: proj.id == id)
49
+ if result is None:
50
+ raise ProjectNotFound(id)
51
+ return result
@@ -0,0 +1,17 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.project.cplusplus** registers a ``"C++"`` projects support.
6
+ """
7
+
8
+ from proj_flow.project import api
9
+
10
+
11
+ @api.project_type.add
12
+ class CPlusPlus(api.ProjectType):
13
+ def __init__(self):
14
+ super().__init__("C++ + CMake + Conan", "cxx")
15
+
16
+
17
+ project = api.get_project_type("cxx")
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.project.data** supports the ``init`` command.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from proj_flow.api import ctx
11
+
12
+
13
+ def get_internal(key: str, value: Any = None):
14
+ return ctx.internals.get(key, value)