Files
nuitka-mirror/tests/reflected/compile_itself.py

540 lines
15 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright 2025, Kay Hayen, mailto:kay.hayen@gmail.com find license text at end of file
"""Test Nuitka compiling itself and compiling itself in compiled form again.
This should not only give no errors, but the same source for modules being
compiled when Nuitka is running compiled and uncompiled, so we can discover
changes in order of execution in this test.
"""
import os
import sys
# Find nuitka package relative to us.
sys.path.insert(
0,
os.path.normpath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
),
)
# isort:start
import difflib
import shutil
import subprocess
import time
from nuitka.reports.CompilationReportReader import getCompilationOutputBinary
from nuitka.tools.Basics import addPYTHONPATH
from nuitka.tools.testing.Common import (
getPythonSysPath,
getTempDir,
my_print,
setup,
test_logger,
withPythonPathChange,
)
from nuitka.utils.Execution import wrapCommandForDebuggerForSubprocess
from nuitka.utils.FileOperations import (
copyTree,
deleteFile,
getFileContents,
getFileList,
getSubDirectories,
listDir,
removeDirectory,
)
from nuitka.utils.Importing import getExtensionModuleSuffix
from nuitka.Version import getCommercialVersion
nuitka_main_path = os.path.join("..", "..", "bin", "nuitka")
tmp_dir = getTempDir()
# Cannot detect this more automatic, so we need to list them, avoiding
# the ones not needed.
PACKAGE_LIST = [
"nuitka",
"nuitka/build",
"nuitka/code_generation",
"nuitka/code_generation/c_types",
"nuitka/code_generation/templates",
"nuitka/containers",
"nuitka/finalizations",
"nuitka/freezer",
"nuitka/importing",
"nuitka/nodes",
"nuitka/nodes/shapes",
"nuitka/optimizations",
"nuitka/options",
"nuitka/pgo",
"nuitka/plugins",
"nuitka/plugins/commercial",
"nuitka/plugins/standard",
"nuitka/reports",
"nuitka/specs",
"nuitka/tree",
"nuitka/format",
"nuitka/package_config",
"nuitka/utils",
]
if not getCommercialVersion():
PACKAGE_LIST.remove("nuitka/plugins/commercial")
exe_suffix = ".exe" if os.name == "nt" else ".bin"
def readSource(filename):
if str is bytes:
return getFileContents(filename, mode="rb")
else:
return getFileContents(filename, encoding="latin1")
def diffRecursive(dir1, dir2):
# Complex in nature, pylint: disable=too-many-branches
done = set()
result = False
for path1, filename in listDir(dir1):
if "cache-" in path1:
continue
path2 = os.path.join(dir2, filename)
done.add(path1)
# Skip these binary files and scons build database of course.
# TODO: Temporary ignore ".bin", until we have something better than marshal which behaves
# differently in compiled Nuitka:
# spell-checker: ignore dblite,sconsign
if filename.endswith(
(
".o",
".os",
".obj",
".dblite",
".tmp",
".sconsign",
".txt",
".bin",
".const",
".exp",
)
):
continue
if "scons-debug" in filename:
continue
if not os.path.exists(path2):
test_logger.warning("Only in %s: %s" % (dir1, filename))
result = False
continue
if os.path.isdir(path1):
r = diffRecursive(path1, path2)
if r:
result = True
elif os.path.isfile(path1):
from_date = time.ctime(os.stat(path1).st_mtime)
to_date = time.ctime(os.stat(path2).st_mtime)
# spell-checker: ignore tofile,fromfiledate,tofiledate
diff = difflib.unified_diff(
a=readSource(path1).splitlines(),
b=readSource(path2).splitlines(),
fromfile=path1,
tofile=path2,
fromfiledate=from_date,
tofiledate=to_date,
n=3,
)
diff_list = list(diff)
if diff_list:
for line in diff_list:
try:
my_print(line)
except UnicodeEncodeError:
my_print(repr(line))
result = True
else:
assert False, path1
for path1, filename in listDir(dir2):
if "cache-" in path1:
continue
path2 = os.path.join(dir2, filename)
if path1 in done:
continue
if not os.path.exists(path1):
test_logger.warning("Only in %s: %s" % (dir2, filename))
result = False
continue
return result
def _traceCompilation(path, pass_number):
test_logger.info("Compiling '%s' (PASS %d)." % (path, pass_number))
def executePASS1():
test_logger.info(
"PASS 1: Compiling to many compiled modules from compiler running from .py files."
)
base_dir = os.path.join("..", "..")
for package in PACKAGE_LIST:
package = package.replace("/", os.path.sep)
source_dir = os.path.join(base_dir, package)
target_dir = package
removeDirectory(
path=target_dir,
logger=test_logger,
ignore_errors=False,
extra_recommendation=None,
)
os.mkdir(target_dir)
for path, filename in listDir(target_dir):
if filename.endswith((".so", ".dylib")):
os.unlink(path)
for path, filename in listDir(source_dir):
if not filename.endswith(".py"):
continue
if filename.startswith(".#"):
continue
if filename != "__init__.py":
_traceCompilation(path=path, pass_number=1)
command = [
os.environ["PYTHON"],
nuitka_main_path,
"--mode=module",
"--nofollow-imports",
"--output-dir=%s" % target_dir,
"--no-pyi-file",
path,
]
command += os.getenv("NUITKA_EXTRA_OPTIONS", "").split()
my_print("Command: ", " ".join(command))
result = subprocess.call(command)
if result != 0:
sys.exit(result)
else:
shutil.copyfile(path, os.path.join(target_dir, filename))
_traceCompilation(path=nuitka_main_path, pass_number=1)
shutil.copyfile(nuitka_main_path, "nuitka-runner.py")
command = [
os.environ["PYTHON"],
nuitka_main_path,
"--nofollow-imports",
"--enable-plugin=pylint-warnings",
"--output-dir=.",
"--python-flag=no_site",
"nuitka-runner.py",
]
command += os.getenv("NUITKA_EXTRA_OPTIONS", "").split()
my_print("Command: ", " ".join(command))
result = subprocess.call(command)
if result != 0:
sys.exit(result)
shutil.move("nuitka-runner" + exe_suffix, "nuitka" + exe_suffix)
scons_inline_copy_path = os.path.join(base_dir, "nuitka", "build", "inline_copy")
if os.path.exists(scons_inline_copy_path):
copyTree(scons_inline_copy_path, os.path.join("nuitka", "build", "inline_copy"))
# Copy required data files.
for filename in (
"nuitka/build/Backend.scons",
"nuitka/plugins/standard/standard.nuitka-package.config.yml",
"nuitka/plugins/standard/stdlib3.nuitka-package.config.yml",
"nuitka/plugins/standard/stdlib2.nuitka-package.config.yml",
):
shutil.copyfile(
os.path.join(base_dir, filename),
filename,
)
copyTree(
os.path.join(base_dir, "nuitka", "code_generation", "templates_c"),
os.path.join("nuitka", "code_generation", "templates_c"),
)
copyTree(
os.path.join(base_dir, "nuitka", "build", "static_src"),
os.path.join("nuitka", "build", "static_src"),
)
copyTree(
os.path.join(base_dir, "nuitka", "build", "include"),
os.path.join("nuitka", "build", "include"),
)
# The data composer tool, use it by source.
copyTree(
os.path.join(base_dir, "nuitka", "tools"),
os.path.join("nuitka", "tools"),
)
test_logger.info("OK.")
def compileAndCompareWith(nuitka, pass_number):
if "PYTHONHASHSEED" not in os.environ:
os.environ["PYTHONHASHSEED"] = "0"
if "PYTHON_FROZEN_MODULES" not in os.environ:
os.environ["PYTHON_FROZEN_MODULES"] = "off"
base_dir = os.path.join("..", "..")
for package in PACKAGE_LIST:
package = package.replace("/", os.path.sep)
source_dir = os.path.join(base_dir, package)
for path, filename in listDir(source_dir):
if not filename.endswith(".py"):
continue
if filename.startswith(".#"):
continue
path = os.path.join(source_dir, filename)
if filename != "__init__.py":
_traceCompilation(path=path, pass_number=pass_number)
target = filename.replace(".py", ".build")
target_dir = os.path.join(tmp_dir, target)
removeDirectory(
logger=test_logger,
path=target_dir,
ignore_errors=False,
extra_recommendation=None,
)
command = [
nuitka,
"--mode=module",
"--enable-plugin=pylint-warnings",
"--output-dir=%s" % tmp_dir,
"--no-pyi-file",
"--nofollow-imports",
path,
]
command += os.getenv("NUITKA_EXTRA_OPTIONS", "").split()
my_print("Command: ", " ".join(command))
exit_nuitka = subprocess.call(command)
# In case of segfault or assertion triggered, run in debugger.
if exit_nuitka in (-11, -6) and sys.platform != "nt":
command2 = wrapCommandForDebuggerForSubprocess(command=command)
subprocess.call(command2)
if exit_nuitka != 0:
my_print("An error exit %s occurred, aborting." % exit_nuitka)
sys.exit(exit_nuitka)
has_diff = diffRecursive(os.path.join(package, target), target_dir)
if has_diff:
sys.exit("There were differences!")
shutil.rmtree(target_dir)
for preferred in (True, False):
target_filename = filename.replace(
".py", getExtensionModuleSuffix(preferred=preferred)
)
deleteFile(
path=os.path.join(tmp_dir, target_filename), must_exist=False
)
def executePASS2():
test_logger.info(
"PASS 2: Compiling from compiler running from entry '.exe' and many extension files."
)
with withPythonPathChange(getPythonSysPath()):
# Windows will load the compiled modules (pyd) only from PYTHONPATH, so we
# have to add it.
if os.name == "nt":
addPYTHONPATH(PACKAGE_LIST)
compileAndCompareWith(
nuitka=os.path.join(".", "nuitka" + exe_suffix), pass_number=2
)
# Cleanup, removing files that will otherwise confuse PASS3.
for filename in getFileList("nuitka", only_suffixes=(".so", ".pyd")):
deleteFile(filename, must_exist=True)
for filename in getSubDirectories("nuitka"):
if filename.endswith(".build"):
removeDirectory(
filename,
logger=test_logger,
ignore_errors=False,
extra_recommendation=None,
)
test_logger.info("OK.")
def executePASS3():
test_logger.info(
"PASS 3: Compiling from compiler running from .py files to single .exe."
)
exe_path = os.path.join(tmp_dir, "nuitka" + exe_suffix)
if os.path.exists(exe_path):
os.unlink(exe_path)
build_path = os.path.join(tmp_dir, "nuitka.build")
if os.path.exists(build_path):
shutil.rmtree(build_path)
path = os.path.join("..", "..", "bin", "nuitka")
_traceCompilation(path=path, pass_number=3)
with withPythonPathChange(os.path.join("..", "..")):
command = [
os.environ["PYTHON"],
nuitka_main_path,
"--output-dir=%s" % build_path,
"--python-flag=-S",
"--follow-imports",
"--include-package=nuitka.plugins.standard",
"--nofollow-import-to=*-postLoad",
"--nofollow-import-to=SCons",
"--nofollow-import-to=pip",
"--report=compilation-report-pass3.xml",
"nuitka-runner.py",
]
my_print("Command: ", " ".join(command))
result = subprocess.call(command)
if result != 0:
sys.exit(result)
shutil.rmtree(build_path)
test_logger.info("OK.")
def executePASS4():
test_logger.info("PASS 4: Compiling the compiler running from single exe.")
exe_path = getCompilationOutputBinary(
compilation_report="compilation-report-pass3.xml",
prefixes=(("${cwd}", os.getcwd()),),
)
with withPythonPathChange(os.path.join("..", "..")):
compileAndCompareWith(exe_path, pass_number=4)
test_logger.info("OK.")
def executePASS5():
my_print(
"PASS 5: Compiling the compiler 'nuitka' package to single extension module."
)
path = os.path.join("..", "..", "nuitka")
command = [
os.environ["PYTHON"],
nuitka_main_path,
"--enable-plugin=pylint-warnings",
"--output-dir=%s" % tmp_dir,
"--include-plugin-dir=%s" % path,
"--nofollow-import-to=nuitka.build.inline_copy",
"--nofollow-import-to=nuitka.build.include",
"--nofollow-import-to=nuitka.build.static_src",
"--nofollow-import-to=nuitka.tools",
"--mode=module",
path,
]
result = subprocess.call(command)
if result != 0:
sys.exit(result)
for preferred in (True, False):
candidate = "nuitka" + getExtensionModuleSuffix(preferred=preferred)
deleteFile(candidate, must_exist=False)
os.unlink(os.path.join(tmp_dir, "nuitka.pyi"))
shutil.rmtree(os.path.join(tmp_dir, "nuitka.build"))
def main():
setup(needs_io_encoding=True)
executePASS1()
executePASS2()
executePASS3()
executePASS4()
executePASS5()
if __name__ == "__main__":
main()
# Python tests originally created or extracted from other peoples work. The
# parts were too small to be protected.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.