From 681b1548f6d2fec78bd9990eb045eaa5ea7284ac Mon Sep 17 00:00:00 2001 From: Elijah ben Izzy Date: Fri, 28 Nov 2025 17:13:14 -0800 Subject: [PATCH 1/3] Updates SVN scripts (#599) Co-authored-by: Elijah ben Izzy --- scripts/release_helper.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/release_helper.py b/scripts/release_helper.py index ce1a7df1d..b08b99ee7 100644 --- a/scripts/release_helper.py +++ b/scripts/release_helper.py @@ -237,7 +237,7 @@ def create_release_artifacts(version, build_wheel=False) -> list[str]: def svn_upload(version, rc_num, archive_files, apache_id): """Uploads the artifacts to the ASF dev distribution repository.""" print("Uploading artifacts to ASF SVN...") - svn_path = f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/apache-burr/{version}-incubating-RC{rc_num}" + svn_path = f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/{version}-incubating-RC{rc_num}" try: # Create a new directory for the release candidate. @@ -245,6 +245,7 @@ def svn_upload(version, rc_num, archive_files, apache_id): [ "svn", "mkdir", + "--parents", "-m", f"Creating directory for {version}-incubating-RC{rc_num}", svn_path, @@ -313,8 +314,11 @@ def generate_email_template(version, rc_num, svn_url): Please download, verify, and test the release candidate. -For testing, please run some of the examples, scripts/qualify.sh has -a sampling of them to run. +For testing use your best judgement. Any of the following will suffice + +1. Build/run the UI following the instructions in scripts/README.md +2. Run the tests in tests/ +3. Import into a jupyter notebook and play around The vote will run for a minimum of 72 hours. Please vote: @@ -430,7 +434,7 @@ def main(): # Upload artifacts # NOTE: You MUST have your SVN client configured to use your Apache ID and have permissions. if dry_run: - svn_url = f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/apache-burr/{version}-incubating-RC{rc_num}" + svn_url = f"https://dist.apache.org/repos/dist/dev/incubator/{PROJECT_SHORT_NAME}/{version}-incubating-RC{rc_num}" print(f"\n[DRY RUN] Would upload artifacts to: {svn_url}") else: svn_url = svn_upload(version, rc_num, archive_files, apache_id) From 01cdaf2e9990cf5677d30f586f87f2c68c655f93 Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Fri, 28 Nov 2025 17:30:13 -0800 Subject: [PATCH 2/3] Hack: manually get around symlink resolution of flit (#600) Flit doesn't resolve symlinks, so in the build process for the wheel we copy over the files we want. We then build the wheel. Afterwards we reset the symlink. This now helps ensure that the convenience package has everything it needs to start the server without issue. --- scripts/build_artifacts.py | 199 +++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/scripts/build_artifacts.py b/scripts/build_artifacts.py index 9a62027af..617873312 100644 --- a/scripts/build_artifacts.py +++ b/scripts/build_artifacts.py @@ -136,16 +136,215 @@ def _clean_ui_build(): print("✓ UI build directory cleaned.\n") +def _replace_symlinks_with_copies(): + """ + Replace symlinked example files/directories with actual copies before building. + Returns a dict mapping paths to their symlink targets (if they were symlinks), + so they can be restored later. + """ + # Files and directories from pyproject.toml lines 266-270 that might be symlinks + example_paths = [ + "examples/__init__.py", + "examples/email-assistant", + "examples/multi-modal-chatbot", + "examples/streaming-fastapi", + "examples/deep-researcher", + ] + + symlink_info = {} # Maps path -> (was_symlink: bool, symlink_target: str or None, is_dir: bool) + + for path in example_paths: + if not os.path.exists(path): + continue + + if os.path.islink(path): + # It's a symlink - we need to replace it with a copy + # Store the original symlink target as read (may be relative) + original_symlink_target = os.readlink(path) + + # Resolve relative symlink targets to absolute paths for copying + if os.path.isabs(original_symlink_target): + resolved_target = original_symlink_target + else: + # Resolve relative to the directory containing the symlink + symlink_dir = os.path.dirname(os.path.abspath(path)) + resolved_target = os.path.join(symlink_dir, original_symlink_target) + resolved_target = os.path.normpath(resolved_target) + + print(f"Found symlink: {path} -> {original_symlink_target}") + print(" Replacing with copy...") + + # Verify the symlink target exists + if not os.path.exists(resolved_target): + print(f" ✗ Warning: Symlink target does not exist: {resolved_target}") + symlink_info[path] = (False, None, False) + continue + + is_directory = os.path.isdir(resolved_target) + + # Remove the symlink + os.remove(path) + + if is_directory: + # For directories, use copytree + shutil.copytree(resolved_target, path, dirs_exist_ok=True) + else: + # For files, use copy2 to preserve metadata + shutil.copy2(resolved_target, path) + + # Store the original symlink target (as it was originally read) + symlink_info[path] = (True, original_symlink_target, is_directory) + print(" ✓ Replaced symlink with copy.\n") + else: + # Not a symlink, nothing to do + symlink_info[path] = (False, None, False) + + return symlink_info + + +def _restore_symlinks(symlink_info): + """ + Restore symlinks that were replaced with copies. + symlink_info: dict from _replace_symlinks_with_copies() + """ + for path, (was_symlink, symlink_target, is_directory) in symlink_info.items(): + if was_symlink and symlink_target: + if os.path.exists(path) and not os.path.islink(path): + # Remove the copy and restore the symlink + print(f"Restoring symlink: {path} -> {symlink_target}") + try: + if is_directory: + shutil.rmtree(path) + else: + os.remove(path) + os.symlink(symlink_target, path) + print(" ✓ Symlink restored.\n") + except Exception as exc: + print(f" ✗ Error restoring symlink: {exc}\n") + + +def _copy_examples_to_burr(): + """ + Copy example directories into burr/examples/ so they're included in the wheel. + Flit wheels only package what's in the burr/ module directory. + If burr/examples exists (as symlink or directory), remove it first to ensure + we copy actual files, not symlinks. + Returns tuple: (copied: bool, was_symlink: bool, symlink_target: str or None) + """ + burr_examples_dir = "burr/examples" + source_examples_dir = "examples" + + if not os.path.exists(source_examples_dir): + print(f"Warning: {source_examples_dir} does not exist. Skipping copy.\n") + return (False, False, None) + + # Check if burr/examples exists and if it's a symlink - we'll need to restore it later + was_symlink = False + symlink_target = None + if os.path.exists(burr_examples_dir): + if os.path.islink(burr_examples_dir): + was_symlink = True + symlink_target = os.readlink(burr_examples_dir) + print(f"Removing existing {burr_examples_dir} symlink (-> {symlink_target})...") + os.remove(burr_examples_dir) + else: + print(f"Removing existing {burr_examples_dir} directory...") + shutil.rmtree(burr_examples_dir) + print(f" ✓ Removed existing {burr_examples_dir}\n") + + print( + f"Copying examples from {source_examples_dir} to {burr_examples_dir} for wheel packaging..." + ) + + # Create burr/examples directory + os.makedirs(burr_examples_dir, exist_ok=True) + + # Copy __init__.py if it exists + init_file = os.path.join(source_examples_dir, "__init__.py") + if os.path.exists(init_file): + shutil.copy2(init_file, os.path.join(burr_examples_dir, "__init__.py")) + + # Copy the specific example directories from pyproject.toml + example_dirs = [ + "email-assistant", + "multi-modal-chatbot", + "streaming-fastapi", + "deep-researcher", + ] + + for example_dir in example_dirs: + source_path = os.path.join(source_examples_dir, example_dir) + dest_path = os.path.join(burr_examples_dir, example_dir) + + if os.path.exists(source_path): + if os.path.isdir(source_path): + shutil.copytree(source_path, dest_path, dirs_exist_ok=True) + else: + shutil.copy2(source_path, dest_path) + print(f" ✓ Copied {example_dir}") + + print(f"✓ Examples copied to {burr_examples_dir}.\n") + return (True, was_symlink, symlink_target) + + +def _remove_examples_from_burr(was_symlink=False, symlink_target=None): + """ + Remove the examples directory from burr/ after building the wheel. + If it was originally a symlink, restore it. + """ + burr_examples_dir = "burr/examples" + if os.path.exists(burr_examples_dir): + print(f"Removing {burr_examples_dir} after wheel build...") + shutil.rmtree(burr_examples_dir) + print(f" ✓ Removed {burr_examples_dir}.\n") + + # Restore the original symlink if it existed + if was_symlink and symlink_target: + print(f"Restoring symlink: {burr_examples_dir} -> {symlink_target}") + try: + os.symlink(symlink_target, burr_examples_dir) + print(" ✓ Symlink restored.\n") + except Exception as exc: + print(f" ✗ Error restoring symlink: {exc}\n") + + def _build_wheel() -> bool: print("Building wheel distribution with 'flit build --format wheel'...") + + # Replace symlinked directories with copies before building + symlink_info = _replace_symlinks_with_copies() + + # Copy examples into burr/ so they're included in the wheel + examples_copied, examples_was_symlink, examples_symlink_target = _copy_examples_to_burr() + try: env = os.environ.copy() env["FLIT_USE_VCS"] = "0" subprocess.run(["flit", "build", "--format", "wheel"], check=True, env=env) print("✓ Wheel build completed successfully.\n") + + # Remove examples from burr/ after successful build (and restore symlink if needed) + if examples_copied: + _remove_examples_from_burr(examples_was_symlink, examples_symlink_target) + + # Restore symlinks after successful build + _restore_symlinks(symlink_info) return True except subprocess.CalledProcessError as exc: print(f"✗ Error building wheel: {exc}") + # Remove examples from burr/ even on error (and restore symlink if needed) + if examples_copied: + _remove_examples_from_burr(examples_was_symlink, examples_symlink_target) + # Restore symlinks even on error + _restore_symlinks(symlink_info) + return False + except Exception as exc: + # Remove examples from burr/ on any other error (and restore symlink if needed) + if examples_copied: + _remove_examples_from_burr(examples_was_symlink, examples_symlink_target) + # Restore symlinks on any other error + print(f"✗ Unexpected error building wheel: {exc}") + _restore_symlinks(symlink_info) return False From 816853508117a6d8d95fa8c1b00409eae52a9510 Mon Sep 17 00:00:00 2001 From: Elijah ben Izzy Date: Fri, 28 Nov 2025 21:12:09 -0800 Subject: [PATCH 3/3] Adds header to all remaining wheel-deployed files (#601) Co-authored-by: Elijah ben Izzy --- .asf.yaml | 17 +++++++++++++++++ README.md | 19 +++++++++++++++++++ burr/tracking/server/requirements-s3.txt | 17 +++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/.asf.yaml b/.asf.yaml index 1c8359c00..98fbcd3d9 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -1,3 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + # https://cwiki.apache.org/confluence/display/INFRA/Git+-+.asf.yaml+features github: diff --git a/README.md b/README.md index 37c096762..c65c035ed 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ + + # Apache Burr (incubating)
diff --git a/burr/tracking/server/requirements-s3.txt b/burr/tracking/server/requirements-s3.txt index e1f4632b5..cd244a6c9 100644 --- a/burr/tracking/server/requirements-s3.txt +++ b/burr/tracking/server/requirements-s3.txt @@ -1,3 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + aerich aiobotocore fastapi-utils