powermake 1.0.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.
@@ -0,0 +1,3 @@
1
+ .vscode
2
+ __pycache__
3
+ build
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.3
2
+ Name: powermake
3
+ Version: 1.0.0
4
+ Summary: Yet another makefile tool. This one is meant to be super fast, super easy and crossplatform while leaving full power to the user at any time.
5
+ Project-URL: Homepage, https://github.com/mactul/powermake
6
+ Project-URL: Issues, https://github.com/mactul/powermake/issues
7
+ Author: Macéo Tuloup
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+
14
+ # PowerMake
15
+
16
+ ## What is PowerMake ?
17
+
18
+ Powermake is an utility to compile C/C++ code, just like Make, Ninja, cmake or xmake.
19
+
20
+ His goal is to give full power to the user, while being cross-platform, easier to use than Make and faster than Ninja.
21
+
22
+ ## For which project is PowerMake suitable ?
23
+
24
+ PowerMake was specifically designed for all the complex projects that have very complicated compilation steps, with a lot of pre-built tasks and which need to be compiled on multiple operating systems with different options.
25
+
26
+ ## Advantages of PowerMake
27
+
28
+ - Extremely fast:
29
+ - PowerMake is approximately 2x faster than Ninja, at least on relatively small projects
30
+ - With a project that has 10 000 lines of C:
31
+ - Ninja takes 1.484 seconds
32
+ - PowerMake takes 0.649 seconds
33
+
34
+ - Cross-Platform:
35
+ - PowerMake is able to detect the compiler installed on your machine and give you an abstraction of the compiler syntax.
36
+ - This currently works well with GCC/G++/Clang/Clang++/MSVC, but other compilers will be add.
37
+ - Because it's written in python it works in almost all machine and you can always write the compilation instructions for your machine and for your compiler.
38
+
39
+ - Gives you complete control of what you are doing. Nothing is hidden and any behavior can be overwritten.
40
+
41
+ ## Disadvantages of PowerMake
42
+
43
+ - PowerMake is very young so it changes a lot at each version and you may have to write some features by yourself (the whole point of PowerMake is that you can write missing features).
44
+
45
+ - Because PowerMake gives you full control, the tool can't really now what's you are doing during the compilation step. For example, if we want to import dependencies from another PowerMake, the only thing we can do for you is running the PowerMake where it stands and scanning his output directory. This works well but has some limitations...
46
+
47
+ ## Philosophy
48
+
49
+ All other Make-like utilities that I know parse a file to understand directives from it.
50
+
51
+ PowerMake does the opposite. You write a python script, you do whatever you like in this script and you call PowerMake functions to help you compiling your code.
52
+ This gives you a complete control; you can retrieve files from the web, read an write files, even train a Neural Network if you want and at any time you can use Powermake functions to help you in your compilation journey.
53
+
54
+ ## Installation
55
+
56
+ Documentation in coming...
57
+
58
+ ## Quick Example
59
+
60
+ This example compile all `.c` and `.cpp` files that are recursively in the same folder as the python script and generate an executable named `program_test`
61
+
62
+ **Warning !** PowerMake calculate all paths from his own location, not the location where it is run.
63
+ For example, `python ./folder/makefile.py` will do the same as `cd ./folder && python ./makefile.py`
64
+
65
+ ```py
66
+ import powermake
67
+
68
+
69
+ def on_build(config: powermake.Config):
70
+
71
+ files = powermake.get_files("**/*.c", "**/*.cpp")
72
+
73
+ objects = powermake.compile_files(config, files)
74
+
75
+ print(powermake.link_files(config, objects))
76
+
77
+
78
+ powermake.run("program_test", build_callback=on_build)
79
+ ```
80
+
81
+ ## Documentation
82
+
83
+ Documentation in coming...
@@ -0,0 +1,70 @@
1
+ # PowerMake
2
+
3
+ ## What is PowerMake ?
4
+
5
+ Powermake is an utility to compile C/C++ code, just like Make, Ninja, cmake or xmake.
6
+
7
+ His goal is to give full power to the user, while being cross-platform, easier to use than Make and faster than Ninja.
8
+
9
+ ## For which project is PowerMake suitable ?
10
+
11
+ PowerMake was specifically designed for all the complex projects that have very complicated compilation steps, with a lot of pre-built tasks and which need to be compiled on multiple operating systems with different options.
12
+
13
+ ## Advantages of PowerMake
14
+
15
+ - Extremely fast:
16
+ - PowerMake is approximately 2x faster than Ninja, at least on relatively small projects
17
+ - With a project that has 10 000 lines of C:
18
+ - Ninja takes 1.484 seconds
19
+ - PowerMake takes 0.649 seconds
20
+
21
+ - Cross-Platform:
22
+ - PowerMake is able to detect the compiler installed on your machine and give you an abstraction of the compiler syntax.
23
+ - This currently works well with GCC/G++/Clang/Clang++/MSVC, but other compilers will be add.
24
+ - Because it's written in python it works in almost all machine and you can always write the compilation instructions for your machine and for your compiler.
25
+
26
+ - Gives you complete control of what you are doing. Nothing is hidden and any behavior can be overwritten.
27
+
28
+ ## Disadvantages of PowerMake
29
+
30
+ - PowerMake is very young so it changes a lot at each version and you may have to write some features by yourself (the whole point of PowerMake is that you can write missing features).
31
+
32
+ - Because PowerMake gives you full control, the tool can't really now what's you are doing during the compilation step. For example, if we want to import dependencies from another PowerMake, the only thing we can do for you is running the PowerMake where it stands and scanning his output directory. This works well but has some limitations...
33
+
34
+ ## Philosophy
35
+
36
+ All other Make-like utilities that I know parse a file to understand directives from it.
37
+
38
+ PowerMake does the opposite. You write a python script, you do whatever you like in this script and you call PowerMake functions to help you compiling your code.
39
+ This gives you a complete control; you can retrieve files from the web, read an write files, even train a Neural Network if you want and at any time you can use Powermake functions to help you in your compilation journey.
40
+
41
+ ## Installation
42
+
43
+ Documentation in coming...
44
+
45
+ ## Quick Example
46
+
47
+ This example compile all `.c` and `.cpp` files that are recursively in the same folder as the python script and generate an executable named `program_test`
48
+
49
+ **Warning !** PowerMake calculate all paths from his own location, not the location where it is run.
50
+ For example, `python ./folder/makefile.py` will do the same as `cd ./folder && python ./makefile.py`
51
+
52
+ ```py
53
+ import powermake
54
+
55
+
56
+ def on_build(config: powermake.Config):
57
+
58
+ files = powermake.get_files("**/*.c", "**/*.cpp")
59
+
60
+ objects = powermake.compile_files(config, files)
61
+
62
+ print(powermake.link_files(config, objects))
63
+
64
+
65
+ powermake.run("program_test", build_callback=on_build)
66
+ ```
67
+
68
+ ## Documentation
69
+
70
+ Documentation in coming...
@@ -0,0 +1,285 @@
1
+ import os
2
+ import sys
3
+ import glob
4
+ import fnmatch
5
+ import subprocess
6
+ import importlib.util
7
+ import __main__ as __makefile__
8
+ from threading import Lock
9
+ from concurrent.futures import ThreadPoolExecutor
10
+
11
+ from .config import Config
12
+ from .operation import Operation, needs_update
13
+ from .args_parser import run, default_on_clean, default_on_install
14
+
15
+
16
+ os.chdir(os.path.dirname(os.path.realpath(__makefile__.__file__)))
17
+
18
+
19
+ def import_module(module_name: str, module_path: str = None):
20
+ """Import a custom module from a path
21
+
22
+ Args:
23
+ module_name (str): The name of the module once it will be imported
24
+ module_path (str, optional): The path of the module, if None, it takes the module_name as a path.
25
+
26
+ Returns:
27
+ module: an module object, that you can use as a namespace
28
+ """
29
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
30
+ module = importlib.util.module_from_spec(spec)
31
+ spec.loader.exec_module(module)
32
+
33
+ return module
34
+
35
+
36
+ def get_files(*patterns: str) -> set[str]:
37
+ """Return all files on the disk that matches one of the patterns given
38
+
39
+ Supported patterns are `*`, like `./*.c` and `**/` like `./**/*.c` to search all .c recursively, starting at `./`
40
+
41
+ Warning: `**.c` will not return all .c recursively, you have to use `**/*.c` for that.
42
+
43
+ Args:
44
+ patterns (str): As many patterns as you want to include
45
+
46
+ Returns:
47
+ set[str]: A set (unordered list) of files. You can use the methods `add` and `update` to add files to this set.
48
+ """
49
+ files = set()
50
+ for pattern in patterns:
51
+ files.update(glob.glob(pattern, recursive=True))
52
+ return files
53
+
54
+
55
+ def filter_files(files: set[str], *patterns: str) -> set[str]:
56
+ """Create a copy of the set `files` with all elements that matches `pattern` removed.
57
+
58
+ This function will equally works if `files` is an iterable but will always returns a set.
59
+
60
+ Args:
61
+ files (set[str]): The set of files to filter
62
+ patterns (str): As many patterns as you want to exclude
63
+
64
+ Returns:
65
+ set[str]: the filtered set
66
+ """
67
+ output = set()
68
+ for file in files:
69
+ for pattern in patterns:
70
+ if not fnmatch.fnmatch(file, pattern):
71
+ output.add(file)
72
+ return output
73
+
74
+
75
+ def file_in_files_set(file: str, files_set: set[str]) -> bool:
76
+ for f in files_set:
77
+ if os.path.samefile(f, file):
78
+ return True
79
+ return False
80
+
81
+
82
+ def compile_files(config: Config, files: set[str], force: bool = None) -> set[str]:
83
+ """Compile each C/C++ file in the `files` set according to the compiler and options stored in `config`
84
+
85
+ The compilation is parallelized with `config.nb_jobs` threads
86
+
87
+ This function will equally works if `files` is an iterable but will always returns a set.
88
+
89
+ Args:
90
+ config (Config): A powermake.Config object, containing all directives for the compilation. Either the one given to the build_callback or a modified copy.
91
+ files (set[str]): A set of files that ends by .c, .cpp, .cc or .C to compile in .o (or the specified compiler equivalent extension)
92
+ force (bool, optional): Whether the function should verify if a file needs to be recompiled or if it should recompile everything.
93
+ If not specified, this parameter takes the value of config.rebuild
94
+
95
+ Raises:
96
+ RuntimeError: if no compiler is found
97
+ ValueError: if `files` contains a file that doesn't ends with .c, .cpp, .cc or .C
98
+
99
+ Returns:
100
+ set[str]: A set of object filepaths generated by by the compilation
101
+ """
102
+ generated_objects: set[str] = set()
103
+ operations: set[Operation] = set()
104
+
105
+ if force is None:
106
+ force = config.rebuild
107
+
108
+ if config.single_file is not None:
109
+ if file_in_files_set(config.single_file, files):
110
+ files = {config.single_file}
111
+ else:
112
+ return set()
113
+
114
+ for file in files:
115
+ output_file = os.path.normpath(config.obj_build_directory + "/" + file.replace("..", "__") + config.c_compiler.obj_extension)
116
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
117
+
118
+ if config.c_compiler is not None:
119
+ c_args = config.c_compiler.format_args(config.defines, config.additional_includedirs, config.c_flags + config.c_cpp_flags)
120
+ else:
121
+ c_args = None
122
+
123
+ if config.cpp_compiler is not None:
124
+ cpp_args = config.cpp_compiler.format_args(config.defines, config.additional_includedirs, config.cpp_flags + config.c_cpp_flags)
125
+ else:
126
+ cpp_args = None
127
+
128
+ if file.endswith(".c"):
129
+ if config.c_compiler is None:
130
+ raise RuntimeError("No C compiler has been specified and the default config didn't find any")
131
+ command = config.c_compiler.basic_compile_command(output_file, file, c_args)
132
+ elif file.endswith((".cpp", ".cc", ".C")):
133
+ if config.cpp_compiler is None:
134
+ raise RuntimeError("No C++ compiler has been specified and the default config didn't find any")
135
+ command = config.cpp_compiler.basic_compile_command(output_file, file, cpp_args)
136
+ else:
137
+ raise ValueError("The file extension %s can't be compiled", (os.path.splitext(file)[1], ))
138
+ operations.add(Operation(output_file, [file], config, command))
139
+
140
+ if config.single_file is not None:
141
+ (op, ) = operations
142
+ op.execute(force)
143
+ exit(0)
144
+
145
+ print_lock = Lock()
146
+ with ThreadPoolExecutor(max_workers=config.nb_jobs) as executor:
147
+ generated_objects = set(executor.map(lambda op: op.execute(force, print_lock), operations))
148
+
149
+ if False in generated_objects:
150
+ generated_objects = False
151
+
152
+ return generated_objects
153
+
154
+
155
+ def archive_files(config: Config, object_files: set[str], archive_name: str = None, force: bool = None) -> str:
156
+ """Create a static library from A set of object files.
157
+
158
+ Args:
159
+ config (Config): A powermake.Config object, containing all directives for the compilation. Either the one given to the build_callback or a modified copy.
160
+ object_files (set[str]): A set of object files, potentially the set generated by `compile_files`
161
+ archive_name (str, optional): The name of the static library you want to create, minus the extension. If None, it will be lib{config.target_name}
162
+ force (bool, optional): Whether the function should verify if the static library needs to be re-archived or if it should re-archive in any case.
163
+ If not specified, this parameter takes the value of config.rebuild
164
+
165
+ Raises:
166
+ RuntimeError: if no archiver is found
167
+
168
+ Returns:
169
+ str: the path of the archive generated
170
+ """
171
+ if force is None:
172
+ force = config.rebuild
173
+
174
+ if archive_name is None:
175
+ archive_name = "lib"+config.target_name
176
+
177
+ if config.archiver is None:
178
+ raise RuntimeError("No archiver has been specified and the default config didn't find any")
179
+ output_file = os.path.normpath(config.lib_build_directory + "/" + archive_name + config.archiver.static_lib_extension)
180
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
181
+ command = config.archiver.basic_archive_command(output_file, object_files)
182
+ return Operation(output_file, object_files, config, command).execute(force=force)
183
+
184
+
185
+ def link_files(config: Config, object_files: set[str], archives: list[str] = [], executable_name: str = None, force: bool = None) -> str:
186
+ """Create an executable from a list of object files and a list of archive files.
187
+
188
+ Args:
189
+ config (Config): A powermake.Config object, containing all directives for the compilation. Either the one given to the build_callback or a modified copy.
190
+ object_files (set[str]): A set of object files, potentially the set generated by `compile_files`
191
+ archives (list[str]): A set of static libraries filepaths
192
+ executable_name (str, optional): The name of the executable you want to create, minus the extension. If None, it will be config.target_name
193
+ force (bool, optional): Whether the function should verify if the executable needs to be re-linked or if it should re-link in any case.
194
+ If not specified, this parameter takes the value of config.rebuild
195
+
196
+ Raises:
197
+ RuntimeError: if no linker is found
198
+
199
+ Returns:
200
+ str: the path of the executable generated
201
+ """
202
+ if force is None:
203
+ force = config.rebuild
204
+
205
+ if executable_name is None:
206
+ executable_name = config.target_name
207
+
208
+ if config.linker is None:
209
+ raise RuntimeError("No linker has been specified and the default config didn't find any")
210
+ output_file = os.path.normpath(config.exe_build_directory + "/" + executable_name + config.linker.exe_extension)
211
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
212
+ command = config.linker.basic_link_command(output_file, object_files, archives)
213
+ return Operation(output_file, object_files.union(archives), config, command).execute(force=force)
214
+
215
+
216
+ def delete_files_from_disk(*filepaths: str) -> None:
217
+ """Delete a set of filepaths from the disk and ignore them if they don't exists
218
+ """
219
+ for filepath in filepaths:
220
+ try:
221
+ os.remove(filepath)
222
+ except OSError:
223
+ pass
224
+
225
+
226
+ def run_another_powermake(config: Config, path: str, debug: bool = None, rebuild: bool = None, verbosity: int = None, nb_jobs: int = None) -> list[str]:
227
+ """Run a powermake from another directory and returns a list of path to all libraries generated
228
+
229
+ Args:
230
+ config (Config): A powermake.Config object, containing all directives for the compilation. Either the one given to the build_callback or a modified copy.
231
+ path (str): The path of the powermake to run
232
+ debug (bool, optional): Whether the other powermake should be run in debug mode.
233
+ If not specified, this parameter takes the value of config.debug
234
+ rebuild (bool, optional): Whether the other powermake should be run in rebuild mode.
235
+ If not specified, this parameter takes the value of config.rebuild
236
+ verbosity (int, optional): With which verbosity level the other powermake should be run.
237
+ If not specified, this parameter takes the value of config.verbosity
238
+ nb_jobs (int, optional): With how many threads the other powermake should be run.
239
+ If not specified, this parameter takes the value of config.nb_jobs
240
+
241
+ Raises:
242
+ RuntimeError: if the other powermake fails
243
+
244
+ Returns:
245
+ list[str]: A list of path to all libraries generated
246
+ """
247
+ if debug is None:
248
+ debug = config.debug
249
+ if rebuild is None:
250
+ rebuild = config.rebuild
251
+ if verbosity is None:
252
+ verbosity = config.verbosity
253
+ if nb_jobs is None:
254
+ nb_jobs = config.nb_jobs
255
+
256
+ command = [sys.executable, path, "--get-lib-build-folder", "-j", str(nb_jobs)]
257
+ if verbosity == 0:
258
+ command.append("-q")
259
+ elif verbosity >= 2:
260
+ command.append("-v")
261
+
262
+ if rebuild:
263
+ command.append("-r")
264
+
265
+ if debug:
266
+ command.append("-d")
267
+
268
+ if config.verbosity >= 1:
269
+ print("Running", path)
270
+ if config.verbosity >= 2:
271
+ print(command)
272
+
273
+ try:
274
+ output = subprocess.check_output(command, encoding="utf-8").splitlines()
275
+ except OSError:
276
+ raise RuntimeError(f"Failed to run powermake {path}")
277
+
278
+ if verbosity != 0:
279
+ for line in output[:-1]:
280
+ print(line)
281
+
282
+ path = output[-1]
283
+ if path != "":
284
+ return [os.path.join(path, file) for file in os.listdir(path)]
285
+ return None
@@ -0,0 +1,33 @@
1
+ # - applexros: arm64 armv7 armv7s i386 x86_64
2
+ # - windows: x86 x64 arm64
3
+ # - appletvos: arm64 armv7 armv7s i386 x86_64
4
+ # - cross: i386 x86_64 arm arm64 mips mips64 riscv riscv64 loong64 s390x ppc ppc64 sh4
5
+ # - mingw: i386 x86_64 arm arm64
6
+ # - msys: i386 x86_64
7
+ # - linux: i386 x86_64 armv7 armv7s arm64-v8a mips mips64 mipsel mips64el loong64
8
+ # - harmony: armeabi-v7a arm64-v8a x86 x86_64
9
+ # - watchos: armv7k i386
10
+ # - wasm: wasm32 wasm64
11
+ # - iphoneos: arm64 x86_64
12
+ # - haiku: i386 x86_64
13
+ # - bsd: i386 x86_64
14
+ # - android: armeabi armeabi-v7a arm64-v8a x86 x86_64 mips mip64
15
+ # - macosx: x86_64 arm64
16
+ # - cygwin: i386 x86_64
17
+
18
+
19
+ def simplify_architecture(architecture: str) -> str:
20
+ arch = architecture.lower()
21
+ if arch in ["x86", "x32", "80x86", "8086", "80386", "i286", "i386", "i486", "i586", "i686", "i786", "amd386", "am386", "amd486", "am486", "amd-k5", "amd-k6", "amd-k7"]:
22
+ return "x86"
23
+
24
+ if arch in ["x86_64", "x86-64", "x64", "amd64", "intel64"]:
25
+ return "x64"
26
+
27
+ if arch in ["arm32", "arm", "armv6", "armv6-m", "armv7a", "armv7s", "armv7m", "armv7r", "armv7-a", "armv7-m", "armv7-r", "armeabi", "armeabi-v7a"]:
28
+ return "arm32"
29
+
30
+ if arch in ["arm64", "armv8-a", "armv8.2-a", "armv8.3-a", "armv8-m", "armv8-r"]:
31
+ return "arm64"
32
+
33
+ return None
@@ -0,0 +1,68 @@
1
+ import abc
2
+
3
+ from .tools import Tool
4
+
5
+
6
+ class Archiver(Tool, abc.ABC):
7
+ static_lib_extension = None
8
+
9
+ def __init__(self, path):
10
+ Tool.__init__(self, path)
11
+
12
+ @abc.abstractmethod
13
+ def basic_archive_command(self, outputfile: str, inputfiles: set[str], args: list[str] = []) -> list[str]:
14
+ return []
15
+
16
+
17
+ class ArchiverGNU(Archiver):
18
+ type = "gnu"
19
+ static_lib_extension = ".a"
20
+
21
+ def __init__(self, path: str = "ar"):
22
+ super().__init__(path)
23
+
24
+ def basic_archive_command(self, outputfile: str, inputfiles: set[str], args: list[str] = []) -> list[str]:
25
+ return [self.path, "-cr", outputfile, *inputfiles, *args]
26
+
27
+
28
+ class ArchiverAR(ArchiverGNU):
29
+ type = "ar"
30
+
31
+ def __init__(self, path: str = "ar"):
32
+ super().__init__(path)
33
+
34
+
35
+ class ArchiverLLVM_AR(ArchiverGNU):
36
+ type = "llvm-ar"
37
+
38
+ def __init__(self, path: str = "llvm-ar"):
39
+ super().__init__(path)
40
+
41
+
42
+ class ArchiverMSVC(Archiver):
43
+ type = "msvc"
44
+ static_lib_extension = ".lib"
45
+
46
+ def __init__(self, path: str = "lib"):
47
+ super().__init__(path)
48
+
49
+ def basic_archive_command(self, outputfile: str, inputfiles: set[str], args: list[str] = []) -> list[str]:
50
+ return [self.path, "/nologo", *args, "/out:"+outputfile, *inputfiles]
51
+
52
+
53
+ _archiver_types: dict[str, Archiver] = {
54
+ "gnu": ArchiverGNU,
55
+ "ar": ArchiverAR,
56
+ "llvm-ar": ArchiverAR,
57
+ "msvc": ArchiverMSVC
58
+ }
59
+
60
+
61
+ def GenericArchiver(archiver_type: str) -> Archiver:
62
+ if archiver_type not in _archiver_types:
63
+ return None
64
+ return _archiver_types[archiver_type]
65
+
66
+
67
+ def get_all_archiver_types() -> set[str]:
68
+ return _archiver_types.keys()