diff --git a/README.rst b/README.rst index 28b320b..2c3d893 100644 --- a/README.rst +++ b/README.rst @@ -8,6 +8,19 @@ Enscons is a Python packaging tool based on `SCons `_. It bu Enscons helps you to build sdists that can be automatically built by pip, and wheels that are independent of enscons. +About this fork +--------------- + +This fork adds the following features to plain enscons: + +* optionally name SConstruct files enscons.py + +* possibility to pass --config-settings of the form 'option=value' into SCons ARGUMENTS + +* possibility to have smaller SConstructs with the help of enscons.helper module (remove boilerplate code) + +* experimentally support pyarmor + What does enscons provide? -------------------------- diff --git a/enscons/api.py b/enscons/api.py index 4c2e4fd..c74b4d2 100644 --- a/enscons/api.py +++ b/enscons/api.py @@ -22,6 +22,8 @@ def _run(alias): try: + if os.path.isfile("enscons.py") and not "-fenscons.py" in sys.argv: + sys.argv.append("-fenscons.py") SCons.Script.Main.main() except SystemExit: pass diff --git a/enscons/helpers.py b/enscons/helpers.py new file mode 100644 index 0000000..1306099 --- /dev/null +++ b/enscons/helpers.py @@ -0,0 +1,261 @@ +""" +This module provides helper functions for packaging python code into wheels using enscons as a build backend. + +A basic pure-python SConstruct may look like the following: + +# SConstruct start + + import enscons + import enscons.helpers + import os + + env, version_file = enscons.helpers.prolog(None, enscons.get_universal_tag()) + package_source = [node for package in env["PACKAGE_METADATA"]["packages"] + for node in enscons.helpers.recursiveGlob(package)] + + # add other files (e.g. json configurations) to the package_source list as needed, you can use + # enscons.helpers.recursiveGlob for this as well + + lib = env.Whl(env["WHEEL_TARGET"], package_source, root=".") + lic_files = [x for x in os.listdir() if x.lower().startswith("license") or x.lower().startswith("notice")] + lib += env.Install(env["DIST_INFO_PATH"], lic_files) + + enscons.helpers.epilog(env, lib, lic_files + version_file) + +# SConstruct end + +If you want to distribute shared objects, dll's or other binary code, you can use this template + +# SConstruct start + + import enscons + improt enscons.helpers + import os + + env, version_file = enscons.helpers.prolog(None, enscons.get_universal_tag()) + package_source = [node for package in env["PACKAGE_METADATA"]["packages"] + for node in enscons.helpers.recursiveGlob(package)] + + # add other files (e.g. json configurations) to the package_source list as needed, you can use + # enscons.helpers.recursiveGlob for this as well + + lib = env.Whl(env["WHEEL_TARGET"], package_source, root=".") + lic_files = [x for x in os.listdir() if x.lower().startswith("license") or x.lower().startswith("notice")] + lib += env.Install(env["DIST_INFO_PATH"], lic_files) + + if not "editable" in COMMAND_LINE_TARGETS: + # assume we only have one package + env["PACKAGE_NAME"] = env["PACKAGE_METADATA"]["packages"][0] + # for each binary source, add the source to the wheel like this + lib += env.Install("$WHEEL_PATH/$PACKAGE_NAME/", binary_source) + + enscons.helpers.epilog(env, lib, lic_files + version_file) + +# SConstruct end + +If you want enable pyarmor, you can use this template: + +# SConstruct start + + import enscons + import enscons.helpers + import os + + env, version_file = enscons.prolog(None, enscons.get_binary_tag()) + py_files = [node for package in env["PACKAGE_METADATA"]["packages"] + for node in enscons.helpers.recursiveGlob(package)] + + lib = env.Whl(env["WHEEL_TARGET"], [], root=".") + pyarmored = enscons.helpers.pyarmor(py_files, env) + + package_source = (pyfiles if pyarmored is None else []) + # add other files (e.g. json configurations) to the package_source list as needed, you can use + # enscons.helpers.recursiveGlob for this as well + + lib += env.Whl(env["WHEEL_TARGET"], package_source, root=".") + lic_files = [x for x in os.listdir() if x.lower().startswith("license") or x.lower().startswith("notice")] + lib += env.Install(env["DIST_INFO_PATH"], lic_files) + if pyarmored is not None: + lib += pyarmored + + enscons.helpers.epilog(env, lib, lic_files + version_file) + +# SConstruct end + +""" + +import os +from pathlib import Path +import shutil +import sys +import tempfile +import traceback +import enscons +import pytoml as toml +from SCons.Script import * + +def recursiveGlob(root, pattern="*.py", ignoreNames=None, expectFile=None): + """ + Helper function for recursively globbing package source files + + :param root: base directory to search + :param pattern: file name pattern (default: *.py) + :param ignoreNames: directory names to ignore (default: ["__pycache__"]) + :param expectFile: directories without at least one of the given files are ignored (default: ["__init__.py"]) + :return: a list of SCons file nodes matching the rules + """ + ignoreNames = ["__pycache__"] if ignoreNames is None else ignoreNames + expectFile = ["__init__.py"] if expectFile is None else [expectFile] if isinstance(expectFile, str) else expectFile + matches = [] + res = Glob(os.path.join(root, pattern)) + for oswroot, oswdirnames, _ in os.walk(root): + for oswdirname in oswdirnames: + if oswdirname in ignoreNames: + print("[%s]: Ignoring %s (directory name is ignored)." % (pattern, os.path.join(oswroot, oswdirname))) + continue + found = any([os.path.isfile(os.path.join(oswroot, oswdirname, fn)) for fn in expectFile]) + if (not found) and len(expectFile) > 0: + print("[%s]: Ignoring %s (expected files %s not found)." % (pattern, os.path.join(oswroot, oswdirname), expectFile)) + continue + res.extend(Glob(os.path.join(oswroot, oswdirname, pattern))) + return res + +def prolog(os_environ_version_key, wheel_tag, is_binary=None, py_project_toml="pyproject.toml"): + """ + Function supporting an enscons build to remove some boilerplate copy/paste stuff. + Use env["PACKAGE_METADATA"] to access the toml metadata. + Use env["WHEEL_TARGET"] to access the target ("purelib" or "platlib") + + :param os_environ_version_key: the name of the environment variable containing the version number (might be None) + :param wheel_tag: the tag for the wheel (you can use enscons.get_abi3_tag() for binary wheels and enscons.get_universal_tag() for pure python wheels) + :param py_project_toml: the name of the pyproject.toml file + :return: scons environment, version_file SCons node + """ + if is_binary is None: + is_binary = "-none-any" not in wheel_tag + + if Dir("#build").exists(): + # these directories might cause troubles -> remove them before doing anything else + print("Removing left-over build directory") + shutil.rmtree(Dir("#build").abspath, ignore_errors=True) + + metadata = dict(toml.load(open("pyproject.toml")))["project"] + if os.path.isfile("VERSION.txt"): + metadata["version"] = open("VERSION.txt").read().strip() + version_file = ["VERSION.txt"] + else: + default_version = "99.99.99" + metadata["version"] = os.environ.get(os_environ_version_key, default_version) if os_environ_version_key is not None else default_version + version_file = None + + env = Environment( + tools=["default", "packaging", "textfile", enscons.generate], + PACKAGE_METADATA=metadata, + WHEEL_TAG=wheel_tag, + ENV=os.environ.copy(), + WHEEL_TARGET="platlib" if is_binary else "purelib" + ) + + if version_file is None: + # version files will be put into the source distribution + version_file = env.Textfile("VERSION.txt", metadata["version"]) + + return env, version_file + +def epilog(env, lib, additional_source_files): + """ + Function supporting an enscons build to remove some boilerplate copy/paste stuff. + + :param env: The SCons environment to be used + :param lib: The lib targets as a source to env.Wheel + :param additional_source_files: additional files to be added to sdist (e.g. LICENSE, NOTICE, ...) + """ + whl = env.WhlFile(lib) + + # Add automatic source files, plus any other needed files. + sdist_source = FindSourceFiles() + ["PKG-INFO", "setup.py", "pyproject.toml"] + additional_source_files + sdist = env.SDist(source=sdist_source) + + env.NoClean(sdist) + env.Alias("sdist", sdist) + + develop = env.Command("#DEVELOP", enscons.egg_info_targets(env), enscons.develop) + env.Alias("develop", develop) + + # needed for pep517 / enscons.api to work + env.Default(whl, sdist) + +def pyarmor_enabled(): + pyarmor_platform = ARGUMENTS.get("pyarmor_platform", None) + pyarmor_license = ARGUMENTS.get("pyarmor_license", None) + pyarmor_cmd = ARGUMENTS.get("pyarmor_cmd", None) + pyarmor_flags = ARGUMENTS.get("pyarmor_flags", None) + if pyarmor_platform is not None or pyarmor_license is not None or pyarmor_cmd is not None or pyarmor_flags is not None: + return True + return False + +def pyarmor(py_files, env): + """ + Function for supporting pyarmor obfuscation. The obfuscation is controlled via the SCons arguments + pyarmor_platform, pyarmor_license, pyarmor_cmd, pyarmor_flags. If at least one of these arguments is + present, then pyarmor is enabled. + + Note: This is a pretty basic usage of pyarmor. More advanced usages with restrict may probably require the + usage of pyarmor projects (https://pyarmor.readthedocs.io/en/v7.7/project.html), which is out of scope here. + + :param py_files: the python files of the project (potentially obfuscated) + :param env: the scons environment where env.Whl already has been executed at least once. + :return: the pyarmored files or None if pyarmor is not configured + """ + pyarmor_platform = ARGUMENTS.get("pyarmor_platform", None) + pyarmor_license = ARGUMENTS.get("pyarmor_license", None) + pyarmor_cmd = ARGUMENTS.get("pyarmor_cmd", None) + pyarmor_flags = ARGUMENTS.get("pyarmor_flags", None) + pyarmored = None + if pyarmor_platform is not None or pyarmor_license is not None or pyarmor_cmd is not None or pyarmor_flags is not None: + if pyarmor_cmd is None: + pyarmor_cmd = "%s -m pyarmor.pyarmor" % sys.executable + pyarmor_args = "" if pyarmor_flags is None else pyarmor_flags + pyarmor_license = [pyarmor_license] if pyarmor_license is not None else [] + #reglic = env.Command(["non_existing_dummy_file"], [pyarmor_license], "%s register ${SOURCES[0]}" % pyarmor_cmd) + if pyarmor_platform is not None: + pyarmor_args += " --platform=%s" % pyarmor_platform + + sources = [] + targets = [] + for f in py_files: + sources.append(f) + targets.append(env["WHEEL_PATH"].File(str(f))) + targets.append(env["WHEEL_PATH"].Dir("ifm_o3r_cmf").Dir("pytransform")) + + def pyarmorify(target, source, env): + try: + with tempfile.TemporaryDirectory() as tmpdir: + first_source = None + for i, tgtfile in enumerate(target[:-1]): + srcfile = str(source[i]) + tgtdir = Path(tmpdir) / str(source[i].dir) + tgtdir.mkdir(parents=True, exist_ok=True) + shutil.copy(srcfile, tgtdir) + if i == 0: + first_source = tgtdir / Path(srcfile).name + if len(source) == len(target): + print("%s register %s" % (pyarmor_cmd, source[-1])) + if env.Execute("%s register %s" % (pyarmor_cmd, source[-1])): + raise RuntimeError("pyarmor register returned error") + elif len(source)+1 != len(target): + raise RuntimeError("Unexpected list lengths") + print("%s obfuscate %s --recursive --output $WHEEL_PATH/${PACKAGE_METADATA['packages'][0]} %s" % (pyarmor_cmd, pyarmor_args, first_source)) + if env.Execute("%s obfuscate %s --recursive --output $WHEEL_PATH/${PACKAGE_METADATA['packages'][0]} %s" % (pyarmor_cmd, pyarmor_args, first_source)): + raise RuntimeError("pyarmor obfuscate returned error") + except Exception as e: + traceback.print_exc() + return 1 + return 0 + + # we are using whole package mode here + pyarmored = env.Command(targets, sources + pyarmor_license, + pyarmorify) + + return pyarmored + diff --git a/pyproject.toml b/pyproject.toml index f3ccdff..270f563 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] -name = "enscons" -description = "Tools for building Python packages with SCons" +name = "enscons-forked" +description = "Tools for building Python packages with SCons (forked for providing some useful features)" version = "0.28.0" -authors = [{ name = "Daniel Holth", email = "dholth@fastmail.fm" }] +authors = [{ name = "Christoph Wiedemann", email = "62332054+cwiede@users.noreply.github.com" }] classifiers = [ "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", @@ -22,7 +22,7 @@ license = "MIT" packages = ["enscons"] readme = "README.rst" src_root = "." -url = "https://github.com/dholth/enscons" +url = "https://github.com/cwiede/enscons" [project.optional-dependencies] cli = ["click"]