From 1157609f7ff990464d39c1f15a8746bd97d589e2 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Sun, 22 Oct 2023 12:30:43 -0400 Subject: [PATCH] Initial commit --- .github/workflows/build.yml | 142 +++++++ .gitignore | 105 +++++ .idea/.gitignore | 3 + .idea/clang-build-ext.iml | 20 + .idea/inspectionProfiles/Project_Default.xml | 12 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/markdown.xml | 9 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + LICENSE | 201 ++++++++++ README.md | 33 ++ build.py | 96 +++++ pyproject.toml | 3 + setup.py | 89 +++++ .../python/clang_build_ext_tests.py | 178 +++++++++ src/integrationtest/resources/.gitignore | 3 + .../resources/extension_1/setup.py | 21 + .../resources/extension_1/src/alib/alib.c | 1 + .../resources/extension_1/src/alib/alib.h | 0 .../extension_1/src/alib/subdir/subdir1.c | 0 .../resources/extension_1/src/alib/subdir1.c | 0 .../resources/extension_1/src/module/module.c | 22 ++ .../extension_1/src/module/subdir/module1.c | 0 .../resources/extension_1/src/shlib/shlib.c | 4 + .../resources/extension_1/src/shlib/shlib.h | 3 + .../karellen/clang_build_ext/__init__.py | 370 ++++++++++++++++++ 27 files changed, 1342 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/clang-build-ext.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.py create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 src/integrationtest/python/clang_build_ext_tests.py create mode 100644 src/integrationtest/resources/.gitignore create mode 100644 src/integrationtest/resources/extension_1/setup.py create mode 100644 src/integrationtest/resources/extension_1/src/alib/alib.c create mode 100644 src/integrationtest/resources/extension_1/src/alib/alib.h create mode 100644 src/integrationtest/resources/extension_1/src/alib/subdir/subdir1.c create mode 100644 src/integrationtest/resources/extension_1/src/alib/subdir1.c create mode 100644 src/integrationtest/resources/extension_1/src/module/module.c create mode 100644 src/integrationtest/resources/extension_1/src/module/subdir/module1.c create mode 100644 src/integrationtest/resources/extension_1/src/shlib/shlib.c create mode 100644 src/integrationtest/resources/extension_1/src/shlib/shlib.h create mode 100644 src/main/python/karellen/clang_build_ext/__init__.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4c64a3f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,142 @@ +name: clang-build-ext +on: + pull_request: + branches: + - master + push: + branches: + - master +jobs: + build-primary: + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + python-version: + - '3.12' + - '3.11' + - '3.10' + - '3.9' + - '3.8' + setuptools-version: + - '68.0' + - '67.0' + - '66.0' + env: + DEPLOY_PYTHONS: "3.11" + DEPLOY_OSES: "Linux" + DEPLOY_SETUPTOOLS: "68.0" + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + - shell: bash + run: | + echo "PYB_EXTRA_ARGS=--no-venvs" >> $GITHUB_ENV + echo "SETUPTOOLS_VER=~=${{matrix.setuptools-version}}" >> $GITHUB_ENV + + - shell: bash + if: | + github.event_name == 'push' && + contains(env.DEPLOY_OSES, runner.os) && + contains(env.DEPLOY_PYTHONS, matrix.python-version) && + contains(env.DEPLOY_SETUPTOOLS, matrix.setuptools-version) + + run: | + echo "PYB_EXTRA_ARGS=+upload --no-venvs" >> $GITHUB_ENV + + - uses: pybuilder/build@master + with: + checkout: false + with-venv: false + python-version: ${{ matrix.python-version }} + pyb-extra-args: ${{ env.PYB_EXTRA_ARGS }} + pre-build: | + $PYTHON -m pip install karellen-llvm-clang[tools] + $PYTHON -c pass + + build-secondary: + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + python-version: + - '3.7' + setuptools-version: + - '67.0' + - '66.0' + - '65.0' + - '64.0' + - '63.0' + - '62.0' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + - shell: bash + run: | + echo "PYB_EXTRA_ARGS=--no-venvs" >> $GITHUB_ENV + echo "SETUPTOOLS_VER=~=${{matrix.setuptools-version}}" >> $GITHUB_ENV + + - uses: pybuilder/build@master + with: + checkout: false + with-venv: false + python-version: ${{ matrix.python-version }} + pyb-extra-args: ${{ env.PYB_EXTRA_ARGS }} + pre-build: | + $PYTHON -m pip install karellen-llvm-clang[tools] + $PYTHON -c pass + + build-experimental: + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + python-version: + - '3.13-dev' + setuptools-version: + - '68.0' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v3 + - shell: bash + run: | + echo "PYB_EXTRA_ARGS=--no-venvs" >> $GITHUB_ENV + echo "SETUPTOOLS_VER=~=${{matrix.setuptools-version}}" >> $GITHUB_ENV + + - uses: pybuilder/build@master + with: + checkout: false + with-venv: false + python-version: ${{ matrix.python-version }} + pyb-extra-args: ${{ env.PYB_EXTRA_ARGS }} + pre-build: | + $PYTHON -m pip install karellen-llvm-clang[tools] + $PYTHON -c pass + + build-summary: + if: success() || failure() + runs-on: ubuntu-latest + name: Build Summary + needs: + - build-primary + - build-secondary + - build-experimental + steps: + - name: Check build matrix status + if: | + needs.build-primary.result != 'success' || + needs.build-secondary.result != 'success' + run: exit 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3956bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ +.pybuilder/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/clang-build-ext.iml b/.idea/clang-build-ext.iml new file mode 100644 index 0000000..39a7cca --- /dev/null +++ b/.idea/clang-build-ext.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..e7eb5eb --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..1e34094 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..3cea97e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..37a5c98 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..025bb93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Karellen, Inc. + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9780713 --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Clang Build Extension + +[![Gitter](https://img.shields.io/gitter/room/karellen/Lobby?logo=gitter)](https://app.gitter.im/#/room/#karellen_Lobby:gitter.im) +[![Build Status](https://img.shields.io/github/actions/workflow/status/karellen/clang-build-ext/build.yml?branch=master)](https://github.com/karellen/clang-build-ext/actions/workflows/build.yml) +[![Coverage Status](https://img.shields.io/coveralls/github/karellen/clang-build-ext/master?logo=coveralls)](https://coveralls.io/r/karellen/clang-build-ext?branch=master) + +[![Clang Build Ext Version](https://img.shields.io/pypi/v/clang-build-ext?logo=pypi)](https://pypi.org/project/clang-build-ext/) +[![Clang Build Ext Python Versions](https://img.shields.io/pypi/pyversions/clang-build-ext?logo=pypi)](https://pypi.org/project/clang-build-ext/) + +[![Clang Build Ext Downloads Per Day](https://img.shields.io/pypi/dd/clang-build-ext?logo=pypi)](https://pypistats.org/packages/clang-build-ext) +[![Clang Build Ext Downloads Per Week](https://img.shields.io/pypi/dw/clang-build-ext?logo=pypi)](https://pypistats.org/packages/clang-build-ext) +[![Clang Build Ext Downloads Per Month](https://img.shields.io/pypi/dm/clang-build-ext?logo=pypi)](https://pypistats.org/packages/clang-build-ext) + +The `clang-build-ext` extension builds Python extensions using a Clang compiler stack. +Either system LLVM/Clang or `karellen-llvm-clang` package can be used. + +Beyond compiler the additional functionality is currently undocumented. + +## How to Use + +Add the followi +```python +from karellen.clang_build_ext import ClangBuildExt, ClangBuildClib + +... + +setup( +... +cmdclass={"build_ext": ClangBuildExt, + "build_clib": ClangBuildClib},) +) + +``` \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..c53caa3 --- /dev/null +++ b/build.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# +# (C) Copyright 2023 Karellen, Inc. (https://www.karellen.co/) +# +# 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. +# + +from os import environ +from pybuilder.core import (use_plugin, init, Author, task, depends, dependents) + +use_plugin("python.install_dependencies") +use_plugin("python.core") +use_plugin("python.integrationtest") +use_plugin("python.flake8") +use_plugin("python.coverage") +use_plugin("python.distutils") +use_plugin("python.pycharm") +use_plugin("python.coveralls") +use_plugin("copy_resources") +use_plugin("filter_resources") + +name = "clang_build_ext" +version = "0.0.1.dev" + +summary = "Clang-based extension builder" +authors = [Author("Karellen, Inc.", "supervisor@karellen.co")] +maintainers = [Author("Arcadiy Ivanov", "arcadiy@karellen.co")] +url = "https://github.com/karellen/clang-build-ext" +urls = { + "Bug Tracker": "https://github.com/karellen/clang-build-ext/issues", + "Source Code": "https://github.com/karellen/clang-build-ext/", + "Documentation": "https://github.com/karellen/clang-build-ext/" +} +license = "Apache License, Version 2.0" + +requires_python = ">=3.7" + +default_task = ["analyze", "publish"] + + +@init(environments="ci") +def init_ci_dependencies(project): + project.build_depends_on("setuptools", environ["SETUPTOOLS_VER"]) + default_task.append("install_ci_dependencies") + + +@task +@depends("install_dependencies") +@dependents("compile_sources") +def install_ci_dependencies(project): + pass + + +@init +def set_properties(project): + project.set_property("coverage_break_build", False) + project.set_property("cram_fail_if_no_tests", False) + + project.set_property("integrationtest_inherit_environment", True) + + project.set_property("copy_resources_target", "$dir_dist/karellen/clang_build_ext") + project.get_property("copy_resources_glob").append("LICENSE") + project.include_file("karellen/clang_build_ext", "LICENSE") + project.set_property("filter_resources_target", "$dir_dist") + project.get_property("filter_resources_glob").append("karellen/clang_build_ext/__init__.py") + + project.set_property("distutils_readme_description", True) + project.set_property("distutils_description_overwrite", True) + project.set_property("distutils_upload_skip_existing", True) + project.set_property("distutils_setup_keywords", ["setuptools", "extension", "cpython" + "clang", "c", "cpp", "cxx", "c++", + "compile"]) + + project.set_property("distutils_entry_points", { + "distutils.commands": ["build_ext = karellen.clang_build_ext:ClangBuildExt"] + }) + + project.set_property("distutils_classifiers", [ + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Operating System :: POSIX :: Linux", + "Topic :: System :: Archiving :: Packaging", + "Topic :: Software Development :: Build Tools", + "Intended Audience :: Developers", + "Development Status :: 4 - Beta" + ]) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..950f549 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["pybuilder>=0.12.0"] +build-backend = "pybuilder.pep517" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..aaff15c --- /dev/null +++ b/setup.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# This file is part of PyBuilder +# +# Copyright 2011-2020 PyBuilder Team +# +# 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. + +# +# This script allows to support installation via: +# pip install git+git://@ +# +# This script is designed to be used in combination with `pip install` ONLY +# +# DO NOT RUN MANUALLY +# + +import os +import subprocess +import sys +import glob +import shutil + +from sys import version_info +py3 = version_info[0] == 3 +py2 = not py3 +if py2: + FileNotFoundError = OSError + + +def install_pyb(): + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", "pybuilder"]) + except subprocess.CalledProcessError as e: + sys.exit(e.returncode) + + +script_dir = os.path.dirname(os.path.realpath(__file__)) +exit_code = 0 + +try: + subprocess.check_call(["pyb", "--version"]) +except FileNotFoundError as e: + if py3 or py2 and e.errno == 2: + install_pyb() + else: + raise +except subprocess.CalledProcessError as e: + if e.returncode == 127: + install_pyb() + else: + sys.exit(e.returncode) + +try: + from pybuilder.cli import main + # verbose, debug, skip all optional... + if main("-v", "-X", "-o", "--reset-plugins", "clean", "package"): + raise RuntimeError("PyBuilder build failed") + + from pybuilder.reactor import Reactor + reactor = Reactor.current_instance() + project = reactor.project + dist_dir = project.expand_path("$dir_dist") + + for src_file in glob.glob(os.path.join(dist_dir, "*")): + file_name = os.path.basename(src_file) + target_file_name = os.path.join(script_dir, file_name) + if os.path.exists(target_file_name): + if os.path.isdir(target_file_name): + shutil.rmtree(target_file_name) + else: + os.remove(target_file_name) + shutil.move(src_file, script_dir) + setup_args = sys.argv[1:] + subprocess.check_call([sys.executable, "setup.py"] + setup_args, cwd=script_dir) +except subprocess.CalledProcessError as e: + exit_code = e.returncode +sys.exit(exit_code) diff --git a/src/integrationtest/python/clang_build_ext_tests.py b/src/integrationtest/python/clang_build_ext_tests.py new file mode 100644 index 0000000..425b11e --- /dev/null +++ b/src/integrationtest/python/clang_build_ext_tests.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# +# (C) Copyright 2022 Karellen, Inc. (https://www.karellen.co/) +# +# 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. +# + +import os +import runpy +import shutil +import sys +import unittest +from os.path import dirname, join as jp, exists +from tempfile import TemporaryDirectory +from sysconfig import get_platform, get_python_version + +PLATFORM = f"{get_platform()}-cpython-{sys.version_info[0]}{sys.version_info[1]}" + + +class ClangBuildExtTest(unittest.TestCase): + def setUp(self) -> None: + self.test_dir = jp(dirname(dirname(__file__)), "resources") + self.target_dir = TemporaryDirectory() + self.src_dir = jp(self.target_dir.name, "src") + self.build_dir = jp(self.target_dir.name, "build") + self.temp_dir = jp(self.target_dir.name, "temp") + self.wheels = set() + + def tearDown(self) -> None: + self.target_dir.cleanup() + for wheel_file in list(self.wheels): + try: + self.uninstall(wheel_file) + except Exception: + sys.excepthook(*sys.exc_info()) + + def build_test(self, dir_name, *extra_args, **env): + src_dir = jp(self.test_dir, dir_name) + shutil.copytree(src_dir, self.src_dir, symlinks=True, ignore_dangling_symlinks=True) + + old_env = dict(os.environ) + old_sys_argv = list(sys.argv) + old_cwd = os.getcwd() + try: + script_path = jp(self.src_dir, "setup.py") + sys.argv.clear() + sys.argv.extend([script_path] + list(extra_args) + + ["-b", self.build_dir, + "-t", self.temp_dir]) + os.chdir(self.src_dir) + os.environ.update(env) + runpy.run_path(script_path) + finally: + os.chdir(old_cwd) + sys.argv.clear() + sys.argv.extend(old_sys_argv) + os.environ.clear() + os.environ.update(old_env) + + def test_with_cmd_line_drakon(self): + self.build_test("extension_1", "build_clib", "build_ext", "-d") + + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir1.bc")) + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/alib.bc")) + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir/subdir1.bc")) + + self.assertTrue(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertTrue(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertTrue(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) + + def test_with_cmd_line_drakon_thin(self): + self.build_test("extension_1", "build_clib", "build_ext", "-d", "-T") + + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir1.bc")) + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/alib.bc")) + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir/subdir1.bc")) + + self.assertTrue(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertTrue(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertTrue(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) + + def test_with_cmd_line_no_drakon_thin(self): + self.build_test("extension_1", "build_clib", "build_ext", "-T") + + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir1.bc")) + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/alib.bc")) + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir/subdir1.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertFalse(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) + + def test_with_env_drakon(self): + self.build_test("extension_1", "build_clib", "build_ext", DRAKON="1") + + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir1.bc")) + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/alib.bc")) + self.assertTrue(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir/subdir1.bc")) + + self.assertTrue(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertTrue(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertTrue(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) + + def test_with_env_thin(self): + self.build_test("extension_1", "build_clib", "build_ext", THIN="1") + + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir1.bc")) + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/alib.bc")) + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir/subdir1.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertFalse(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) + + def test_with_cmd_line_no_drakon_no_thin(self): + self.build_test("extension_1", "build_clib", "build_ext") + + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir1.bc")) + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/alib.bc")) + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir/subdir1.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertFalse(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) + + def test_clib_with_cmd_line_drakon(self): + self.build_test("extension_1", "build_clib", "-d") + + self.assertTrue(exists(f"{self.temp_dir}/src/alib/subdir1.bc")) + self.assertTrue(exists(f"{self.temp_dir}/src/alib/alib.bc")) + self.assertTrue(exists(f"{self.temp_dir}/src/alib/subdir/subdir1.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertFalse(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) + + def test_clib_with_cmd_line_drakon_thin(self): + self.build_test("extension_1", "build_clib", "-d", "-T") + + self.assertTrue(exists(f"{self.temp_dir}/src/alib/subdir1.bc")) + self.assertTrue(exists(f"{self.temp_dir}/src/alib/alib.bc")) + self.assertTrue(exists(f"{self.temp_dir}/src/alib/subdir/subdir1.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertFalse(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) + + def test_with_compiler_override(self): + self.build_test("extension_1", "build_clib", "build_ext", "-c", "unix") + + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir1.bc")) + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/alib.bc")) + self.assertFalse(exists(f"{self.src_dir}/build/temp.{PLATFORM}/src/alib/subdir/subdir1.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/shlib/shlib.bc")) + + self.assertFalse(exists(f"{self.temp_dir}/src/module/module.bc")) + self.assertFalse(exists(f"{self.temp_dir}/src/module/subdir/module1.bc")) +if __name__ == "__main__": + unittest.main() diff --git a/src/integrationtest/resources/.gitignore b/src/integrationtest/resources/.gitignore new file mode 100644 index 0000000..67497ec --- /dev/null +++ b/src/integrationtest/resources/.gitignore @@ -0,0 +1,3 @@ +**/build/ +**/dist/ +**/*.egg-info/ diff --git a/src/integrationtest/resources/extension_1/setup.py b/src/integrationtest/resources/extension_1/setup.py new file mode 100644 index 0000000..068a0f3 --- /dev/null +++ b/src/integrationtest/resources/extension_1/setup.py @@ -0,0 +1,21 @@ +from setuptools import setup, Extension +from setuptools.extension import Library + +from karellen.clang_build_ext import ClangBuildExt, ClangBuildClib + +setup(name="test", + version="1.0.0", + description="Python test module", + author="Karellen, Inc.", + author_email="supervisor@karellen.co", + ext_modules=[Library("shlib", + ["src/shlib/*.c", "src/shlib/**/*.c"]), + Extension("test", + ["src/module/*.c", "src/module/**/*.c"], + ), + ], + libraries=[("alib", {"sources": ["src/alib/*.c", "src/alib/**/*.c"]}) + ], + cmdclass={"build_ext": ClangBuildExt, + "build_clib": ClangBuildClib}, + ) diff --git a/src/integrationtest/resources/extension_1/src/alib/alib.c b/src/integrationtest/resources/extension_1/src/alib/alib.c new file mode 100644 index 0000000..c8f8250 --- /dev/null +++ b/src/integrationtest/resources/extension_1/src/alib/alib.c @@ -0,0 +1 @@ +#include "alib.h" \ No newline at end of file diff --git a/src/integrationtest/resources/extension_1/src/alib/alib.h b/src/integrationtest/resources/extension_1/src/alib/alib.h new file mode 100644 index 0000000..e69de29 diff --git a/src/integrationtest/resources/extension_1/src/alib/subdir/subdir1.c b/src/integrationtest/resources/extension_1/src/alib/subdir/subdir1.c new file mode 100644 index 0000000..e69de29 diff --git a/src/integrationtest/resources/extension_1/src/alib/subdir1.c b/src/integrationtest/resources/extension_1/src/alib/subdir1.c new file mode 100644 index 0000000..e69de29 diff --git a/src/integrationtest/resources/extension_1/src/module/module.c b/src/integrationtest/resources/extension_1/src/module/module.c new file mode 100644 index 0000000..89c97a2 --- /dev/null +++ b/src/integrationtest/resources/extension_1/src/module/module.c @@ -0,0 +1,22 @@ +#include + +static PyObject *method_test(PyObject *self, PyObject *args) { + return PyLong_FromLong(0l); +} + +static PyMethodDef TestMethods[] = { + {"test", method_test, METH_VARARGS, "Python test function"}, + {NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef testModule = { + PyModuleDef_HEAD_INIT, + "test", + "Python test module", + -1, + TestMethods +}; + +PyMODINIT_FUNC PyInit_test(void) { + return PyModule_Create(&testModule); +} diff --git a/src/integrationtest/resources/extension_1/src/module/subdir/module1.c b/src/integrationtest/resources/extension_1/src/module/subdir/module1.c new file mode 100644 index 0000000..e69de29 diff --git a/src/integrationtest/resources/extension_1/src/shlib/shlib.c b/src/integrationtest/resources/extension_1/src/shlib/shlib.c new file mode 100644 index 0000000..f469d0c --- /dev/null +++ b/src/integrationtest/resources/extension_1/src/shlib/shlib.c @@ -0,0 +1,4 @@ +#include "shlib.h" + +EXPORT void foo() { +} \ No newline at end of file diff --git a/src/integrationtest/resources/extension_1/src/shlib/shlib.h b/src/integrationtest/resources/extension_1/src/shlib/shlib.h new file mode 100644 index 0000000..b5becee --- /dev/null +++ b/src/integrationtest/resources/extension_1/src/shlib/shlib.h @@ -0,0 +1,3 @@ +#define EXPORT __attribute__((visibility("default"))) + +EXPORT void foo(); diff --git a/src/main/python/karellen/clang_build_ext/__init__.py b/src/main/python/karellen/clang_build_ext/__init__.py new file mode 100644 index 0000000..d79ba55 --- /dev/null +++ b/src/main/python/karellen/clang_build_ext/__init__.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# +# (C) Copyright 2023 Karellen, Inc. (https://www.karellen.co/) +# +# 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. +# + +import os +import subprocess +import sys +from contextlib import contextmanager +from distutils import ccompiler +from distutils import log +from distutils.debug import DEBUG +from distutils.errors import DistutilsExecError +from distutils.spawn import find_executable +from distutils.unixccompiler import UnixCCompiler +from distutils.util import split_quoted +from glob import glob +from os.path import exists, dirname, commonpath +from tempfile import TemporaryDirectory + +from setuptools.command.build_clib import build_clib as _build_clib +from setuptools.command.build_ext import build_ext as _build_ext + +COMMON_OPTIONS = [ + ("drakon", "d", + "build extension with Drakon enhancements"), + ("thin", "T", + "build thin static libraries") +] + +COMMON_BOOLEAN_OPTIONS = [ + "drakon", "thin" +] + + +class ClangCCompiler(UnixCCompiler): + executables = { + 'preprocessor': ["clang", "-E"], + 'compiler': ["clang"], + 'compiler_so': ["clang"], + 'compiler_cxx': ["clang-cpp"], + 'linker_so': ["clang", "-shared", "-fuse-ld=lld"], + 'linker_exe': ["clang", "-fuse-ld=lld"], + 'archiver': ["llvm-ar", "rcs"], + 'ranlib': None, + 'objcopy': ["llvm-objcopy"], + 'readelf': ["llvm-readelf"] + } + + def __init__(self, verbose=0, dry_run=0, force=0, drakon=False, thin=False): + self.drakon = drakon + self.thin = thin + super().__init__(verbose, dry_run, force) + self.verbose = verbose or False + + def link( + self, + target_desc, + objects, + output_filename, + output_dir=None, + libraries=None, + library_dirs=None, + runtime_library_dirs=None, + export_symbols=None, + debug=0, + extra_preargs=None, + extra_postargs=None, + build_temp=None, + target_lang=None, + ): + super().link(target_desc, objects, output_filename, output_dir, libraries, library_dirs, runtime_library_dirs, + export_symbols, debug, extra_preargs, extra_postargs, build_temp, target_lang) + + if not self.drakon: + return + + libraries, library_dirs, runtime_library_dirs = self._fix_lib_args(libraries, + library_dirs, + runtime_library_dirs) + + def get_section_name(lib_name, lib_file, source_lib): + common_path = commonpath((lib_file, source_lib)) + if common_path and not common_path.endswith(os.sep): + common_path += os.sep + return f"{lib_name}//{lib_file[len(common_path):]}" + + add_bc_files = {} + for obj in objects: + bc_file = f"{obj[:-2]}.bc" + log.debug("Adding %s", bc_file) + add_bc_files[bc_file] = get_section_name("", bc_file, commonpath(objects)) + + lib_extract_paths = {} + try: + archiver = self.archiver[0] + for lib in libraries: + for lib_dir in library_dirs: + lib_path = f"{lib_dir}{os.sep}lib{lib}.a" + if exists(lib_path): + thin_lib = False + with open(lib_path, "rb") as f: + if f.read(7) == b"!": + thin_lib = True + + lib_extract_paths[lib_path] = lib_extract_dir = TemporaryDirectory() + log.debug(f"Processing {'thin ' if thin_lib else ''}library %s", lib_path) + lib_files = self.spawn_out([archiver, "t", lib_path]).splitlines() + if thin_lib: + for lib_file in lib_files: + if not lib_file.endswith(".bc"): + continue + log.debug("Adding %s", lib_file) + add_bc_files[lib_file] = get_section_name(lib, lib_file, lib_path) + else: + files_in_ar = {} + for l in lib_files: + if l in files_in_ar: + files_in_ar[l] += 1 + else: + files_in_ar[l] = 1 + + for file in files_in_ar.keys(): + if not file.endswith(".bc"): + continue + + count = files_in_ar[file] + for i in range(1, count + 1): + extracted_name = f"{lib_extract_dir.name}{os.sep}{file[0:-3]}" \ + f"{f'.{i!s}' if count > 1 else ''}.bc" + log.debug("Extracting %s", extracted_name) + parents_dir = dirname(file) + os.makedirs(f"{lib_extract_dir.name}{os.sep}{parents_dir}", exist_ok=True) + self.spawn( + [archiver, "--output", lib_extract_dir.name, "xN", str(i), lib_path, file]) + if count > 1: + self.move_file(f"{lib_extract_dir.name}{os.sep}{file}", extracted_name) + add_bc_files[extracted_name] = get_section_name(lib, + extracted_name[len( + lib_extract_dir.name) + 1:], + lib_path) + + break + + cmd_line = self.objcopy[:] + for bc_file, bc_name in add_bc_files.items(): + section_name = f".drakon.{bc_name}" + cmd_line.extend(["--add-section", f"{section_name}={bc_file}", + "--set-section-flags", f"{section_name}=noload,readonly,contents"]) + cmd_line.append(output_filename) + self.spawn(cmd_line) + + finally: + for lib, path in lib_extract_paths.items(): + path.cleanup() + + def create_static_lib(self, objects, output_libname, output_dir=None, debug=0, target_lang=None): + # Add all the bytecode into the ar library + if self.drakon: + new_objects = objects[:] + for obj in objects: + new_objects.append(f"{obj[:-2]}.bc") + objects = new_objects + + super().create_static_lib(objects, output_libname, output_dir, debug, target_lang) + + def _get_cc_args(self, pp_opts, debug, before): + cc_args = super()._get_cc_args(pp_opts, debug, before) + if self.drakon: + cc_args = ["--save-temps=obj", "-fno-discard-value-names"] + cc_args + return cc_args + + def set_executable(self, key, value): + if isinstance(value, str): + value = split_quoted(value) + + default_executable = self.executables.get(key) + if default_executable: + value[0] = default_executable[0] + + if self.thin and key == "archiver": + value = value[:] + value.append("--thin") + + setattr(self, key, value) + + def spawn_out(self, cmd, search_path=1, verbose=0, dry_run=0, + env=None, text=True, stdout=subprocess.PIPE): # pragma: no cover + """Run another program, specified as a command list 'cmd', in a new process. + + 'cmd' is just the argument list for the new process, ie. + cmd[0] is the program to run and cmd[1:] are the rest of its arguments. + There is no way to run a program with a name different from that of its + executable. + + If 'search_path' is true (the default), the system's executable + search path will be used to find the program; otherwise, cmd[0] + must be the exact path to the executable. If 'dry_run' is true, + the command will not actually be run. + + Raise DistutilsExecError if running the program fails in any way; just + return on success. + """ + # cmd is documented as a list, but just in case some code passes a tuple + # in, protect our %-formatting code against horrible death + cmd = list(cmd) + + log.info(subprocess.list2cmdline(cmd)) + if dry_run: + return + + if search_path: + executable = find_executable(cmd[0]) + if executable is not None: + cmd[0] = executable + + env = env if env is not None else dict(os.environ) + + if sys.platform == 'darwin': + from distutils.util import MACOSX_VERSION_VAR, get_macosx_target_ver + + macosx_target_ver = get_macosx_target_ver() + if macosx_target_ver: + env[MACOSX_VERSION_VAR] = macosx_target_ver + + try: + proc = subprocess.run(cmd, env=env, + stdout=stdout, check=False, universal_newlines=text) + exitcode = proc.returncode + except OSError as exc: + if not DEBUG: + cmd = cmd[0] + raise DistutilsExecError( + "command {!r} failed: {}".format(cmd, exc.args[-1]) + ) from exc + + if exitcode: + if not DEBUG: + cmd = cmd[0] + raise DistutilsExecError( + "command {!r} failed with exit code {}".format(cmd, exitcode) + ) + if stdout == subprocess.PIPE: + return proc.stdout + + +class ClangBuildExt(_build_ext): + user_options = list(_build_ext.user_options) + COMMON_OPTIONS + boolean_options = list(_build_ext.boolean_options) + COMMON_BOOLEAN_OPTIONS + + def initialize_options(self) -> None: + super().initialize_options() + + self.drakon = None + self.thin = None + + def finalize_options(self) -> None: + with self.customized_compiler(): + self.set_undefined_options( + 'build', + ('compiler', 'compiler'), + ) + + if self.compiler is None: + self.compiler = "clang" + + if self.drakon is None: + self.drakon = os.environ.get("DRAKON", False) + + if self.thin is None: + self.thin = os.environ.get("THIN", False) + + super().finalize_options() + + def run(self): + with self.customized_compiler(): + super().run() + + def build_extension(self, ext): + sources = ext.sources + try: + ext.sources = [] + for src in sources: + ext.sources.extend(glob(src)) + super().build_extension(ext) + finally: + ext.source = sources + + def new_compiler(self, plat=None, compiler=None, verbose=0, dry_run=0, force=0): + if compiler == "clang": + return ClangCCompiler(None, dry_run, force, drakon=self.drakon, thin=self.thin) + return self._old_new_compiler(plat, compiler, verbose, dry_run, force) + + @contextmanager + def customized_compiler(self): + self._old_new_compiler = _old_new_compiler = ccompiler.new_compiler + ccompiler.new_compiler = self.new_compiler + from setuptools.command import build_ext + _old_build_ext_new_compiler = build_ext.new_compiler + build_ext.new_compiler = self.new_compiler + + try: + yield + finally: + ccompiler.new_compiler = _old_new_compiler + build_ext.new_compiler = _old_build_ext_new_compiler + + +class ClangBuildClib(_build_clib): + user_options = list(_build_clib.user_options) + COMMON_OPTIONS + boolean_options = list(_build_clib.boolean_options) + COMMON_BOOLEAN_OPTIONS + + def initialize_options(self) -> None: + super().initialize_options() + self.drakon = None + self.thin = None + + def finalize_options(self) -> None: + self.set_undefined_options( + 'build_ext', + ('drakon', 'drakon'), + ('thin', 'thin'), + ('compiler', 'compiler') + ) + + with self.customized_compiler(): + super().finalize_options() + + def new_compiler(self, plat=None, compiler=None, verbose=0, dry_run=0, force=0): + if compiler == "clang": + return ClangCCompiler(None, dry_run, force, drakon=self.drakon, thin=self.thin) + return self._old_new_compiler(plat, compiler, verbose, dry_run, force) + + @contextmanager + def customized_compiler(self): + from distutils import ccompiler + self._old_new_compiler = _old_new_compiler = ccompiler.new_compiler + ccompiler.new_compiler = self.new_compiler + try: + yield + finally: + ccompiler.new_compiler = _old_new_compiler + + def run(self): + with self.customized_compiler(): + super().run() + + def build_libraries(self, libraries): + new_libraries = [] + for lib_name, sources_map in libraries: + sources = sources_map.get("sources") + if sources: + new_sources = [] + for src in sources: + new_sources.extend(glob(src)) + new_libraries.append((lib_name, {"sources": new_sources})) + + super().build_libraries(new_libraries)