makepatch 0.0.1__py3-none-any.whl → 0.2.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.
- makepatch/__hooks__.py +7 -0
- makepatch/__init__.py +2 -0
- makepatch/_config.py +49 -8
- makepatch/_types/__init__.pyi +1 -0
- makepatch/_types/hatch.pyi +57 -0
- makepatch/core/__init__.py +1 -0
- makepatch/core/_patcher.py +125 -0
- makepatch/hooks/__init__.py +1 -1
- makepatch/hooks/_patch.py +37 -65
- makepatch/scripts/__init__.py +28 -0
- makepatch/scripts/_rebuild.py +78 -0
- makepatch-0.2.0.dist-info/METADATA +23 -0
- makepatch-0.2.0.dist-info/RECORD +15 -0
- {makepatch-0.0.1.dist-info → makepatch-0.2.0.dist-info}/WHEEL +1 -1
- makepatch-0.2.0.dist-info/entry_points.txt +6 -0
- makepatch/__hook__.py +0 -7
- makepatch-0.0.1.dist-info/METADATA +0 -8
- makepatch-0.0.1.dist-info/RECORD +0 -10
- makepatch-0.0.1.dist-info/entry_points.txt +0 -7
makepatch/__hooks__.py
ADDED
makepatch/__init__.py
CHANGED
makepatch/_config.py
CHANGED
|
@@ -1,18 +1,59 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from collections.abc import Iterable
|
|
2
|
-
from
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import final, Annotated, ClassVar
|
|
3
5
|
|
|
4
6
|
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic.json_schema import SkipJsonSchema
|
|
5
8
|
|
|
9
|
+
if __debug__ and __import__("typing").TYPE_CHECKING:
|
|
10
|
+
|
|
11
|
+
from makepatch._types.hatch import HatchHookInterface
|
|
6
12
|
|
|
7
|
-
__config = None
|
|
8
13
|
|
|
9
14
|
@final
|
|
10
|
-
class
|
|
15
|
+
class PatcherConfig(BaseModel):
|
|
16
|
+
__INTERNAL: ClassVar[object] = object()
|
|
17
|
+
__INSTANCE: ClassVar[PatcherConfig | None] = None
|
|
18
|
+
|
|
11
19
|
module_source: Annotated[str, Field(alias="module-source")]
|
|
12
20
|
excludes: Annotated[Iterable[str], Field(default_factory=frozenset)]
|
|
13
|
-
dev_excludes: Annotated[Iterable[str], Field(alias="
|
|
21
|
+
dev_excludes: Annotated[Iterable[str], Field(alias="dev-excludes", default_factory=frozenset)]
|
|
22
|
+
|
|
23
|
+
module_name: Annotated[SkipJsonSchema[str], Field(exclude=True)]
|
|
24
|
+
project_root: Annotated[SkipJsonSchema[Path], Field(exclude=True)]
|
|
25
|
+
|
|
26
|
+
def __init__(self, obj: object, /, **kwargs):
|
|
27
|
+
if obj is not self.__INTERNAL:
|
|
28
|
+
raise RuntimeError("Security error: External initialization is prohibited")
|
|
29
|
+
if "/" in kwargs["module-source"]:
|
|
30
|
+
_, kwargs["module_name"] = str(kwargs["module-source"]).rsplit(os.sep, 1)
|
|
31
|
+
else:
|
|
32
|
+
kwargs["module_name"] = kwargs["module-source"]
|
|
33
|
+
super().__init__(**kwargs)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_hatch(cls, hook: HatchHookInterface, /) -> PatcherConfig:
|
|
37
|
+
if PatcherConfig.__INSTANCE:
|
|
38
|
+
raise RuntimeError(f"{cls.__name__} already initialized")
|
|
39
|
+
|
|
40
|
+
config = hook.config["options"]
|
|
41
|
+
config["project_root"] = Path(hook.root).resolve()
|
|
42
|
+
PatcherConfig.__INSTANCE = PatcherConfig(PatcherConfig.__INTERNAL, **config)
|
|
43
|
+
assert PatcherConfig.__INSTANCE
|
|
44
|
+
return PatcherConfig.__INSTANCE
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_pyproject(cls, root: Path, /):
|
|
48
|
+
pyproject_toml = (root / "pyproject.toml").resolve()
|
|
49
|
+
if not pyproject_toml.exists() or not pyproject_toml.is_file():
|
|
50
|
+
raise FileNotFoundError(f"{pyproject_toml} does not exist or is not a file")
|
|
14
51
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
52
|
+
import tomllib
|
|
53
|
+
try:
|
|
54
|
+
with open(pyproject_toml, 'rb') as f:
|
|
55
|
+
config = tomllib.load(f)["tool"]["hatch"]["build"]["hooks"]["makepatch"]["options"]
|
|
56
|
+
config["project_root"] = Path(root).resolve()
|
|
57
|
+
return PatcherConfig(PatcherConfig.__INTERNAL, **config)
|
|
58
|
+
except tomllib.TOMLDecodeError as e:
|
|
59
|
+
raise RuntimeError(f"Cannot parse pyproject.toml") from e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .hatch import HatchHookInterface
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Protocol, Any
|
|
2
|
+
|
|
3
|
+
from hatchling.bridge.app import Application
|
|
4
|
+
from hatchling.metadata.core import ProjectMetadata
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HatchHookInterface(Protocol):
|
|
8
|
+
@property
|
|
9
|
+
def app(self, /) -> Application:
|
|
10
|
+
"""
|
|
11
|
+
An instance of [Application](../utilities.md#hatchling.bridge.app.Application).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def root(self, /) -> str:
|
|
16
|
+
"""
|
|
17
|
+
The root of the project tree.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def config(self, /) -> dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
The cumulative hook configuration.
|
|
24
|
+
|
|
25
|
+
```toml config-example
|
|
26
|
+
[tool.hatch.build.hooks.<PLUGIN_NAME>]
|
|
27
|
+
[tool.hatch.build.targets.<TARGET_NAME>.hooks.<PLUGIN_NAME>]
|
|
28
|
+
```
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def metadata(self, /) -> ProjectMetadata:
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def directory(self, /) -> str:
|
|
37
|
+
"""
|
|
38
|
+
The build directory.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def target_name(self, /) -> str:
|
|
43
|
+
"""
|
|
44
|
+
The plugin name of the build target.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def dependencies(self, /) -> list[str]: # noqa: PLR6301
|
|
48
|
+
"""
|
|
49
|
+
A list of extra [dependencies](../../config/dependency.md) that must be installed
|
|
50
|
+
prior to builds.
|
|
51
|
+
|
|
52
|
+
!!! warning
|
|
53
|
+
- For this to have any effect the hook dependency itself cannot be dynamic and
|
|
54
|
+
must always be defined in `build-system.requires`.
|
|
55
|
+
- As the hook must be imported to call this method, imports that require these
|
|
56
|
+
dependencies must be evaluated lazily.
|
|
57
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._patcher import PatchApplier
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import final
|
|
5
|
+
|
|
6
|
+
from makepatch._config import PatcherConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if __debug__ and __import__("typing").TYPE_CHECKING:
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Final
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@final
|
|
15
|
+
class PatchApplier:
|
|
16
|
+
if __debug__ and __import__("typing").TYPE_CHECKING:
|
|
17
|
+
__slots__ = (
|
|
18
|
+
"__config",
|
|
19
|
+
"__excludes",
|
|
20
|
+
"__excludes_dev",
|
|
21
|
+
"__patches",
|
|
22
|
+
"__src",
|
|
23
|
+
"__sources",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: PatcherConfig, /) -> None:
|
|
27
|
+
self.__config: Final = config
|
|
28
|
+
self.__excludes: Final = set[str]()
|
|
29
|
+
self.__excludes_dev: Final = set[str]()
|
|
30
|
+
|
|
31
|
+
# resolve patches
|
|
32
|
+
self.__patches: Final = self.__resolve_path(config.project_root / "patches")
|
|
33
|
+
|
|
34
|
+
# INTENDED: required for later use
|
|
35
|
+
self.__src: Path | None = None
|
|
36
|
+
self.__sources: Path | None = None
|
|
37
|
+
|
|
38
|
+
def setup_required(self, /):
|
|
39
|
+
return not (self.__config.project_root / "work/patched").resolve().exists()
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def __resolve_path(path: Path, /):
|
|
43
|
+
path = path.resolve()
|
|
44
|
+
if not path.exists() or not path.is_dir():
|
|
45
|
+
raise RuntimeError(f"{path} does not exist or is not a directory")
|
|
46
|
+
return path
|
|
47
|
+
|
|
48
|
+
def resolve_src(self, /):
|
|
49
|
+
"""
|
|
50
|
+
Resolves `module` src root, where the codes are placed
|
|
51
|
+
"""
|
|
52
|
+
assert not self.__src
|
|
53
|
+
if not self.__sources:
|
|
54
|
+
self.__sources = self.__resolve_path(self.__config.project_root / "work/sources")
|
|
55
|
+
assert self.__sources
|
|
56
|
+
src = self.__src = self.__resolve_path(self.__sources / self.__config.module_source)
|
|
57
|
+
|
|
58
|
+
for glob in self.__config.excludes:
|
|
59
|
+
dirname = glob.split(os.sep, 1)[0]
|
|
60
|
+
for excluded in src.rglob(glob):
|
|
61
|
+
if (rel_str := str(excluded.relative_to(src))).startswith(dirname):
|
|
62
|
+
self.__excludes.add(rel_str)
|
|
63
|
+
|
|
64
|
+
def resolve_sources(self, /):
|
|
65
|
+
"""
|
|
66
|
+
Resolves entire git sources
|
|
67
|
+
"""
|
|
68
|
+
assert not self.__sources
|
|
69
|
+
sources = self.__sources = self.__resolve_path(self.__config.project_root / "work/sources")
|
|
70
|
+
|
|
71
|
+
for glob in self.__config.dev_excludes:
|
|
72
|
+
dirname = glob.split(os.sep, 1)[0]
|
|
73
|
+
for excluded in sources.rglob(glob):
|
|
74
|
+
if (rel_str := str(excluded.relative_to(sources))).startswith(dirname):
|
|
75
|
+
self.__excludes_dev.add(rel_str)
|
|
76
|
+
|
|
77
|
+
def copy(self, dst: Path, /):
|
|
78
|
+
if self.__excludes_dev:
|
|
79
|
+
src = self.__sources
|
|
80
|
+
excludes = self.__excludes_dev
|
|
81
|
+
filter_ = lambda path: path.is_relative_to(self.__src) and (str(path.relative_to(self.__src)) in self.__excludes)
|
|
82
|
+
else:
|
|
83
|
+
src = self.__src
|
|
84
|
+
excludes = self.__excludes
|
|
85
|
+
filter_ = None
|
|
86
|
+
|
|
87
|
+
assert src and excludes
|
|
88
|
+
for src_file in src.rglob("*"):
|
|
89
|
+
if not src_file.is_file():
|
|
90
|
+
assert src_file.exists()
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
rel = src_file.relative_to(src)
|
|
94
|
+
if (str(rel) in excludes) or (filter_ and filter_(src_file)):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
dst_file = dst / rel
|
|
98
|
+
if dst_file.exists():
|
|
99
|
+
raise RuntimeError(f"Failed to copy file {rel}: file exists")
|
|
100
|
+
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
shutil.copy2(src_file, dst_file)
|
|
102
|
+
|
|
103
|
+
def apply(self, dst: Path, /) -> bool:
|
|
104
|
+
total = []
|
|
105
|
+
for patch in self.__patches.rglob("*.patch"):
|
|
106
|
+
rel_patch = patch.relative_to(self.__patches)
|
|
107
|
+
total.append(rel_patch)
|
|
108
|
+
|
|
109
|
+
rel_dst = rel_patch.with_suffix("") # foo.py.patch -> foo.py
|
|
110
|
+
dst_file = dst / rel_dst
|
|
111
|
+
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
|
|
113
|
+
res = subprocess.run(
|
|
114
|
+
["patch", "--unified", str(dst_file), str(patch)],
|
|
115
|
+
capture_output=True,
|
|
116
|
+
text=True,
|
|
117
|
+
)
|
|
118
|
+
if res.returncode != 0:
|
|
119
|
+
print(f"Failed to apply {rel_patch}:\n\t"
|
|
120
|
+
+ "\n\t".join(res.stdout.splitlines()))
|
|
121
|
+
break
|
|
122
|
+
else:
|
|
123
|
+
print(f"Successfully applied {len(total)} patches")
|
|
124
|
+
return True
|
|
125
|
+
return False
|
makepatch/hooks/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from ._patch import
|
|
1
|
+
from ._patch import PatcherBuildHook
|
makepatch/hooks/_patch.py
CHANGED
|
@@ -1,88 +1,60 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from pathlib import Path
|
|
3
1
|
from typing import override
|
|
4
2
|
|
|
5
3
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
6
4
|
|
|
7
|
-
import shutil
|
|
5
|
+
import shutil
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
from makepatch._config import PatcherConfig
|
|
8
|
+
from makepatch.core import PatchApplier
|
|
11
9
|
|
|
10
|
+
if __debug__ and __import__("typing").TYPE_CHECKING:
|
|
11
|
+
from typing import Any, Final
|
|
12
12
|
|
|
13
|
-
class ClippyPatchHook(BuildHookInterface):
|
|
14
|
-
@property
|
|
15
|
-
def __root(self):
|
|
16
|
-
return Path(self.root)
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
with open(self.__root / "pyproject.toml", "rb") as f:
|
|
21
|
-
pyproject = tomllib.load(f)
|
|
14
|
+
class PatcherBuildHook(BuildHookInterface):
|
|
15
|
+
PLUGIN_NAME = "makepatch"
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return config["module-source"], frozenset(config.get("excludes", set()))
|
|
17
|
+
def __init__(self, /, *args, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
self.__config: Final = PatcherConfig.from_hatch(self)
|
|
20
|
+
self.__patcher: Final = PatchApplier(self.__config)
|
|
28
21
|
|
|
29
22
|
@override
|
|
30
23
|
def initialize(self, version: str, build_data: dict[str, Any]) -> None:
|
|
31
|
-
|
|
24
|
+
config = self.__config
|
|
25
|
+
patcher = self.__patcher
|
|
26
|
+
|
|
27
|
+
if patcher.setup_required():
|
|
28
|
+
patcher.resolve_sources()
|
|
29
|
+
patcher.resolve_src()
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
dst = config.project_root / "work/patched"
|
|
32
|
+
if dst.exists():
|
|
33
|
+
if not dst.is_dir(): raise RuntimeError(f"{dst} is not a directory")
|
|
34
|
+
shutil.rmtree(dst)
|
|
35
|
+
dst.mkdir(parents=True)
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
patcher.copy(dst)
|
|
38
|
+
patcher.apply(dst / config.module_source)
|
|
39
|
+
self.__success = False
|
|
40
|
+
return
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
if not patches.exists(): raise RuntimeError(f"{patches} not exists")
|
|
43
|
-
elif not patches.is_dir(): raise RuntimeError(f"{patches} is not a directory")
|
|
42
|
+
patcher.resolve_src()
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
module_name = module_name[4:]
|
|
47
|
-
dst = self.__root / "src" / module_name
|
|
44
|
+
dst = self.__dst = config.project_root / "src" / config.module_name
|
|
48
45
|
if dst.exists():
|
|
49
46
|
if not dst.is_dir(): raise RuntimeError(f"{dst} is not a directory")
|
|
50
47
|
shutil.rmtree(dst)
|
|
51
48
|
dst.mkdir(parents=True)
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if not src_file.is_file(): continue
|
|
56
|
-
|
|
57
|
-
rel = src_file.relative_to(src)
|
|
58
|
-
if str(rel) in excludes: continue
|
|
59
|
-
|
|
60
|
-
dst_file = dst / rel
|
|
61
|
-
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
-
shutil.copy2(src_file, dst_file)
|
|
63
|
-
|
|
64
|
-
# Apply patches
|
|
65
|
-
for patch_file in sorted(patches.rglob("*.patch")):
|
|
66
|
-
rel_patch = patch_file.relative_to(patches)
|
|
67
|
-
# e.g. foo.py.patch → foo.py
|
|
68
|
-
target_rel = rel_patch.with_suffix("")
|
|
69
|
-
target = dst / target_rel
|
|
70
|
-
|
|
71
|
-
if not target.parent.exists():
|
|
72
|
-
target.parent.mkdir(parents=True, exist_ok=False)
|
|
73
|
-
|
|
74
|
-
result = subprocess.run(
|
|
75
|
-
["patch", "--unified", str(target), str(patch_file)],
|
|
76
|
-
capture_output=True,
|
|
77
|
-
text=True,
|
|
78
|
-
)
|
|
79
|
-
if result.returncode != 0:
|
|
80
|
-
raise RuntimeError(f"Failed to apply {rel_patch}:\n{result.stderr}")
|
|
50
|
+
patcher.copy(dst)
|
|
51
|
+
self.__success = patcher.apply(dst)
|
|
81
52
|
|
|
53
|
+
@override
|
|
82
54
|
def finalize(self, version: str, build_data: dict[str, Any], artifact_path: str) -> None:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
55
|
+
if not self.__success:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if not self.__dst.exists() or not self.__dst.is_dir():
|
|
59
|
+
raise RuntimeError(f"{self.__dst} does not exists or is not a directory")
|
|
60
|
+
shutil.rmtree(self.__dst)
|
makepatch/scripts/__init__.py
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from makepatch._config import PatcherConfig
|
|
5
|
+
from makepatch.core import PatchApplier
|
|
6
|
+
|
|
7
|
+
if __debug__ and __import__("typing").TYPE_CHECKING:
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from ._rebuild import rebuild_patches
|
|
12
|
+
|
|
13
|
+
def init_patches():
|
|
14
|
+
root: Final = Path(__import__("os").getcwd())
|
|
15
|
+
config: Final = PatcherConfig.from_pyproject(root)
|
|
16
|
+
patcher: Final = PatchApplier(config)
|
|
17
|
+
|
|
18
|
+
patcher.resolve_sources()
|
|
19
|
+
patcher.resolve_src()
|
|
20
|
+
|
|
21
|
+
dst = root / "work" / "patched"
|
|
22
|
+
if dst.exists():
|
|
23
|
+
if not dst.is_dir(): raise RuntimeError(f"{dst} is not a directory")
|
|
24
|
+
shutil.rmtree(dst)
|
|
25
|
+
dst.mkdir(parents=True)
|
|
26
|
+
|
|
27
|
+
patcher.copy(dst)
|
|
28
|
+
exit(not patcher.apply(dst / config.module_source))
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from makepatch._config import PatcherConfig
|
|
8
|
+
|
|
9
|
+
if __debug__ and __import__("typing").TYPE_CHECKING:
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def rebuild_patches():
|
|
14
|
+
TIMESTAMP = re.compile(r"\t\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{9} \+\d{4}")
|
|
15
|
+
ARGS = (
|
|
16
|
+
"--ignore-trailing-space",
|
|
17
|
+
"--strip-trailing-cr",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
root: Final = Path(os.getcwd())
|
|
21
|
+
config: Final = PatcherConfig.from_pyproject(root)
|
|
22
|
+
|
|
23
|
+
ref = root / "work/sources" / config.module_source
|
|
24
|
+
if not ref.exists() or not ref.is_dir():
|
|
25
|
+
raise RuntimeError(f"{ref} does not exists or is not a directory")
|
|
26
|
+
|
|
27
|
+
src = root / "work/patched" / config.module_source
|
|
28
|
+
if not ref.exists() or not ref.is_dir():
|
|
29
|
+
raise RuntimeError(f"{ref} does not exists or is not a directory")
|
|
30
|
+
|
|
31
|
+
patches = root / "patches"
|
|
32
|
+
if patches.exists():
|
|
33
|
+
if not patches.is_dir(): raise RuntimeError(f"{patches} is not a directory")
|
|
34
|
+
__import__("shutil").rmtree(patches)
|
|
35
|
+
patches.mkdir(parents=True)
|
|
36
|
+
|
|
37
|
+
excludes = set()
|
|
38
|
+
for glob in config.excludes:
|
|
39
|
+
dirname = glob.split(os.sep, 1)[0]
|
|
40
|
+
for excluded in src.rglob(glob):
|
|
41
|
+
if (rel_str := str(excluded.relative_to(src))).startswith(dirname):
|
|
42
|
+
excludes.add(rel_str)
|
|
43
|
+
|
|
44
|
+
generated = []
|
|
45
|
+
for target_file in sorted(src.rglob("*")):
|
|
46
|
+
if not target_file.is_file(): continue
|
|
47
|
+
if target_file.suffix == "pyc": continue
|
|
48
|
+
if (target_file.parent.name.startswith("__") and
|
|
49
|
+
target_file.parent.name.endswith("__")):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
rel_file = target_file.relative_to(src)
|
|
53
|
+
if str(rel_file) in excludes: continue
|
|
54
|
+
|
|
55
|
+
edited = target_file.relative_to(root)
|
|
56
|
+
origin = (ref / rel_file).relative_to(root)
|
|
57
|
+
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
("diff", "--unified", *ARGS, "--new-file", str(origin), str(edited)),
|
|
60
|
+
capture_output=True,
|
|
61
|
+
text=True,
|
|
62
|
+
)
|
|
63
|
+
if result.returncode == 0:
|
|
64
|
+
continue
|
|
65
|
+
elif result.returncode != 1:
|
|
66
|
+
raise RuntimeError(f"Failed to generate a patch file for {edited} (return code {result.returncode}):"
|
|
67
|
+
f"\n{result.stderr}")
|
|
68
|
+
else:
|
|
69
|
+
patch = patches / rel_file.with_suffix(rel_file.suffix + ".patch")
|
|
70
|
+
patch.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
with open(patch, "x") as f:
|
|
72
|
+
l = result.stdout.splitlines(keepends=True)
|
|
73
|
+
l[0] = TIMESTAMP.sub("", l[0])
|
|
74
|
+
l[1] = TIMESTAMP.sub("", l[1])
|
|
75
|
+
f.write(''.join(l))
|
|
76
|
+
generated.append(patch)
|
|
77
|
+
else:
|
|
78
|
+
print(f"Successfully generated {len(generated)} patches")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: makepatch
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Hatch build hook to easily hard-fork packages with patches!
|
|
5
|
+
Project-URL: source, https://github.com/AlphaKR93/packages/tree/main/python/makepatch
|
|
6
|
+
Project-URL: issues, https://github.com/AlphaKR93/packages/issues
|
|
7
|
+
Author-email: Alpha <dev@alpha93.kr>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Environment :: Plugins
|
|
12
|
+
Classifier: Framework :: Hatch
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: hatchling>=1.29.0
|
|
22
|
+
Requires-Dist: pydantic>=2.13.0
|
|
23
|
+
Requires-Dist: typing-extensions>=4.15.0; python_version < '3.15'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
makepatch/__hooks__.py,sha256=Twwvsdvf5-wFb4aqUD9ApiTj_hpf_kOh_nedyX4a5yE,163
|
|
2
|
+
makepatch/__init__.py,sha256=VGfjjIEpPnILrFlZqLzYhbUL6tqmhx1-e9uZOuVpQh4,86
|
|
3
|
+
makepatch/_config.py,sha256=OA0SPKb8R5Q00cs6DwDu79kN6gW8vnDrKGvp6DCUfG8,2492
|
|
4
|
+
makepatch/_types/__init__.pyi,sha256=SlDWSyq8IncNQKcgvjc9F7jyqKjTCSLW4HLTxgsSYcM,39
|
|
5
|
+
makepatch/_types/hatch.pyi,sha256=MWAwNXZx1RXrdZ8sVLY8pinoOleSajjDatylnFsLprQ,1604
|
|
6
|
+
makepatch/core/__init__.py,sha256=F943qvL-FwY0zkSuMxHuVzG55EVDICJYouAE1-3NrOU,36
|
|
7
|
+
makepatch/core/_patcher.py,sha256=m5IsO66AC40i7rpHH1y5PE5VFW390svgZxA4SIw1930,4403
|
|
8
|
+
makepatch/hooks/__init__.py,sha256=I7QNThkPOzPtvQiocyUoQKKdWNYWk5wiF-FVRz4LTjo,38
|
|
9
|
+
makepatch/hooks/_patch.py,sha256=kSuzOjyeYTWEO457e5ikZI5RN8KGn22n5BVQEj1GOL8,1971
|
|
10
|
+
makepatch/scripts/__init__.py,sha256=dggERiHtxr-wWJ6sHvfsfW3a9a3Io3CmNd7xvIC5AnY,769
|
|
11
|
+
makepatch/scripts/_rebuild.py,sha256=iisw0dza8cr4o_FBvzcrNGmxeeje98XW2g6dHlJqsJ0,2810
|
|
12
|
+
makepatch-0.2.0.dist-info/METADATA,sha256=Cp_Qi2UyIpNqoKsvTNebZNtXQT2XQrTIOgnmPZI_hRE,1003
|
|
13
|
+
makepatch-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
14
|
+
makepatch-0.2.0.dist-info/entry_points.txt,sha256=tDk_T8AJNcjy0KhBEXxf9wZBIAhv3l4pGekKOqXpNpY,159
|
|
15
|
+
makepatch-0.2.0.dist-info/RECORD,,
|
makepatch/__hook__.py
DELETED
makepatch-0.0.1.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
makepatch/__hook__.py,sha256=ov-ZExdRe2dhvnocD01_58Z37eBryz8sTaTAbFEHM-4,171
|
|
2
|
-
makepatch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
makepatch/_config.py,sha256=GlyVVm9ZsolzDKSiupoTMTmOHVSNCMdyo-_AzArw3Wo,565
|
|
4
|
-
makepatch/hooks/__init__.py,sha256=8JvonoGcrgihzhk7eUIwQPwr2oTL58IfF44tAk2vOYc,37
|
|
5
|
-
makepatch/hooks/_patch.py,sha256=xjaKYDJkhekGfBomXOKH5pOiP209hNtt6G4kHv-kfQs,3335
|
|
6
|
-
makepatch/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
makepatch-0.0.1.dist-info/WHEEL,sha256=fWriCkzqm-pffF5af4gJC9iI5FMFaJTuN9UxxxzOmdY,81
|
|
8
|
-
makepatch-0.0.1.dist-info/entry_points.txt,sha256=8xpxWiGS9XHoBCcyoVcfkRfjW2WnvamePMfI3_WJsOs,141
|
|
9
|
-
makepatch-0.0.1.dist-info/METADATA,sha256=2kQkR1g2KEOP011WEC73brgoEzEW_wM-HQsAhJbm6Q8,242
|
|
10
|
-
makepatch-0.0.1.dist-info/RECORD,,
|