From b9fb82c71f64716cf764aa660158de608c791cea Mon Sep 17 00:00:00 2001 From: jernejfrank Date: Fri, 21 Aug 2026 23:12:54 +0100 Subject: [PATCH] Create sdist license The root level license involves packages excluded in sdist; this adjust the release helper to create a temp dir and copying over the necessary artifacts for creating the sdist. Flint only copies LICENSE so the rename happens during copy. --- LICENSE-sdist | 228 +++++++++++++++++++++++++++++++++++ scripts/apache_release.py | 83 ++++++++----- tests/test_apache_release.py | 49 ++++++++ tests/test_release_config.py | 17 +++ 4 files changed, 347 insertions(+), 30 deletions(-) create mode 100644 LICENSE-sdist diff --git a/LICENSE-sdist b/LICENSE-sdist new file mode 100644 index 000000000..476a56a5b --- /dev/null +++ b/LICENSE-sdist @@ -0,0 +1,228 @@ + 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 [yyyy] [name of copyright owner] + + 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. + +------------------------------- + +examples/deep-researcher prompts.py and deep_researcher_utils.py are copied from +https://github.com/langchain-ai/local-deep-researcher and are licensed under the MIT License. + +MIT License + +Copyright (c) 2024 Lance Martin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/apache_release.py b/scripts/apache_release.py index e8659ef00..a07f83529 100644 --- a/scripts/apache_release.py +++ b/scripts/apache_release.py @@ -683,21 +683,18 @@ def _create_git_archive( # ============================================================================ -def _remove_ui_build_artifacts() -> None: - """Remove pre-built UI artifacts to ensure clean build.""" - ui_build_dir = os.path.join("burr", "tracking", "server", "build") - if os.path.exists(ui_build_dir): - print(f" Removing UI build artifacts: {ui_build_dir}") - shutil.rmtree(ui_build_dir) - print(" ✓ UI build artifacts removed") - - def _build_sdist_from_git(version: str, output_dir: str = "dist") -> str: - """Build source distribution from git using flit.""" + """Build the sdist from a staged copy of the committed source tree. + + The complete source release and the Python sdist contain different files, + so each needs a LICENSE describing its own contents. Keep the repository's + LICENSE intact for the source release and install LICENSE-sdist as LICENSE + only in the isolated tree used to build the sdist. + """ _print_step(1, 2, "Building sdist with flit") + output_dir = os.path.abspath(output_dir) os.makedirs(output_dir, exist_ok=True) - _remove_ui_build_artifacts() _check_git_working_tree() env = os.environ.copy() @@ -705,30 +702,56 @@ def _build_sdist_from_git(version: str, output_dir: str = "dist") -> str: source_epoch = _source_date_epoch(version, output_dir) if source_epoch is not None: env["SOURCE_DATE_EPOCH"] = str(source_epoch) - _run_command( - ["flit", "build", "--format", "sdist"], - description="Running flit build --format sdist...", - error_message="Failed to build sdist", - success_message="flit sdist created successfully", - env=env, - ) - # Find and rename sdist - expected_pattern = f"dist/apache_burr-{version.lower()}.tar.gz" - sdist_files = glob.glob(expected_pattern) + with tempfile.TemporaryDirectory(prefix="apache-burr-sdist-") as temp_dir: + temp_path = Path(temp_dir) + source_tree = temp_path / "source" + source_tree.mkdir() + source_archive = temp_path / "source.tar" - if not sdist_files: - _fail(f"Could not find sdist: {expected_pattern}") + _run_command( + ["git", "archive", "HEAD", "--format=tar", "--output", str(source_archive)], + description="Staging committed source files...", + error_message="Failed to stage source files for the sdist", + ) + unpack_options: dict[str, Any] = {} + if sys.version_info >= (3, 12): + # This archive was just produced from our own Git repository. Be + # explicit on newer Python versions while retaining Python 3.9-3.11 + # compatibility, where shutil has no filter argument. + unpack_options["filter"] = "fully_trusted" + shutil.unpack_archive(str(source_archive), str(source_tree), format="tar", **unpack_options) + + sdist_license = source_tree / "LICENSE-sdist" + if not sdist_license.is_file(): + _fail("LICENSE-sdist is missing from the committed source tree") + shutil.copyfile(sdist_license, source_tree / "LICENSE") + sdist_license.unlink() + + # A clean git archive normally has no ignored UI build output, but + # remove this defensively in case it becomes tracked in the future. + shutil.rmtree(source_tree / "burr" / "tracking" / "server" / "build", ignore_errors=True) - original_sdist = sdist_files[0] - apache_sdist = os.path.join( - output_dir, f"apache-burr-{version.lower()}-incubating-sdist.tar.gz" - ) + _run_command( + ["flit", "build", "--format", "sdist"], + description="Running flit build --format sdist...", + error_message="Failed to build sdist", + success_message="flit sdist created successfully", + env=env, + cwd=source_tree, + ) - if os.path.exists(apache_sdist): - os.remove(apache_sdist) + original_sdist = source_tree / "dist" / f"apache_burr-{version.lower()}.tar.gz" + if not original_sdist.is_file(): + _fail(f"Could not find sdist: {original_sdist}") + + apache_sdist = os.path.join( + output_dir, f"apache-burr-{version.lower()}-incubating-sdist.tar.gz" + ) + if os.path.exists(apache_sdist): + os.remove(apache_sdist) + shutil.move(original_sdist, apache_sdist) - shutil.move(original_sdist, apache_sdist) print(f" ✓ Renamed to: {os.path.basename(apache_sdist)}") return apache_sdist diff --git a/tests/test_apache_release.py b/tests/test_apache_release.py index f43867d75..2762d5e71 100644 --- a/tests/test_apache_release.py +++ b/tests/test_apache_release.py @@ -17,7 +17,9 @@ import hashlib import importlib.util +import subprocess import sys +import tarfile from argparse import Namespace from pathlib import Path @@ -37,6 +39,53 @@ def _load_release_module(): release = _load_release_module() +def test_build_sdist_uses_artifact_specific_license(monkeypatch, tmp_path): + project_files = { + "LICENSE": "full source license\nwebsite/src/components/ui/\n", + "LICENSE-sdist": "sdist license\nexamples/deep-researcher\n", + "pyproject.toml": '[project]\nname = "apache-burr"\n', + } + output_dir = tmp_path / "artifacts" + + def fake_check_git_working_tree(): + return None + + def fake_run_command(command, **kwargs): + if command[:3] == ["git", "archive", "HEAD"]: + archive_path = Path(command[command.index("--output") + 1]) + with tarfile.open(archive_path, "w") as archive: + for name, contents in project_files.items(): + source = tmp_path / name + source.write_text(contents, encoding="utf-8") + archive.add(source, arcname=name) + elif command == ["flit", "build", "--format", "sdist"]: + source_tree = Path(kwargs["cwd"]) + assert (source_tree / "LICENSE").read_text(encoding="utf-8") == project_files[ + "LICENSE-sdist" + ] + assert not (source_tree / "LICENSE-sdist").exists() + + built_sdist = source_tree / "dist" / "apache_burr-0.42.0.tar.gz" + built_sdist.parent.mkdir() + with tarfile.open(built_sdist, "w:gz") as archive: + archive.add(source_tree / "LICENSE", arcname="apache_burr-0.42.0/LICENSE") + else: + pytest.fail(f"Unexpected command: {command}") + return subprocess.CompletedProcess(command, 0) + + monkeypatch.setattr(release, "_check_git_working_tree", fake_check_git_working_tree) + monkeypatch.setattr(release, "_source_date_epoch", lambda version, output_dir: None) + monkeypatch.setattr(release, "_run_command", fake_run_command) + + artifact = Path(release._build_sdist_from_git("0.42.0", str(output_dir))) + + assert artifact == output_dir / "apache-burr-0.42.0-incubating-sdist.tar.gz" + with tarfile.open(artifact, "r:gz") as archive: + license_text = archive.extractfile("apache_burr-0.42.0/LICENSE").read().decode() + assert license_text == project_files["LICENSE-sdist"] + assert all(not name.endswith("/LICENSE-sdist") for name in archive.getnames()) + + def _write_artifact_set(directory: Path, version: str, wheel_name: str = None) -> None: wheel_name = wheel_name or f"apache_burr-{version}-py3-none-any.whl" artifact_names = [ diff --git a/tests/test_release_config.py b/tests/test_release_config.py index 5c5c71b8d..8f7bbd104 100644 --- a/tests/test_release_config.py +++ b/tests/test_release_config.py @@ -34,6 +34,23 @@ tomllib = None +def test_sdist_license_only_covers_files_shipped_in_sdist(): + project_root = Path(__file__).parent.parent + source_license = (project_root / "LICENSE").read_text(encoding="utf-8") + sdist_license = (project_root / "LICENSE-sdist").read_text(encoding="utf-8") + + assert "examples/deep-researcher" in sdist_license + assert "website/" not in sdist_license + assert "Magic UI" not in sdist_license + assert "shadcn" not in sdist_license + + # The full source archive still ships the website, so its LICENSE must + # retain the corresponding third-party license entries. + assert "website/" in source_license + assert "Magic UI" in source_license + assert "shadcn" in source_license + + @pytest.mark.skipif(sys.version_info < (3, 11), reason="tomllib requires Python 3.11+") def test_examples_include_exclude_coverage(): """