Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .asf.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
<!--
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.
-->

# <img src="https://github.com/user-attachments/assets/2ab9b499-7ca2-4ae9-af72-ccc775f30b4e" width=25 height=25/> Apache Burr (incubating)

<div>
Expand Down
17 changes: 17 additions & 0 deletions burr/tracking/server/requirements-s3.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
199 changes: 199 additions & 0 deletions scripts/build_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
12 changes: 8 additions & 4 deletions scripts/release_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,15 @@ 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.
subprocess.run(
[
"svn",
"mkdir",
"--parents",
"-m",
f"Creating directory for {version}-incubating-RC{rc_num}",
svn_path,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
Loading