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 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 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)