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)