diff --git a/.github/workflows/conda-pack-build.yml b/.github/workflows/conda-pack-build.yml index b9dae37b..76ff476d 100644 --- a/.github/workflows/conda-pack-build.yml +++ b/.github/workflows/conda-pack-build.yml @@ -65,7 +65,7 @@ jobs: if: steps.should_build.outputs.should_build == 'true' uses: conda-incubator/setup-miniconda@v3 with: - miniforge-version: latest + miniconda-version: latest python-version: '3.11.11' channels: conda-forge,robostack-staging,uni-lab,defaults channel-priority: strict @@ -74,16 +74,11 @@ jobs: auto-update-conda: false show-channel-urls: true - - name: Install conda-pack - if: steps.should_build.outputs.should_build == 'true' - run: | - conda install -c conda-forge conda-pack -y - - - name: Install unilabos and dependencies + - name: Install conda-pack, unilabos and dependencies if: steps.should_build.outputs.should_build == 'true' run: | echo "Installing unilabos and dependencies to unilab environment..." - conda install uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y + conda install uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y - name: Get latest ros-humble-unilabos-msgs version if: steps.should_build.outputs.should_build == 'true' @@ -231,12 +226,14 @@ jobs: run: | echo "==========================================" if [ "${{ matrix.platform }}" == "win-64" ]; then - echo "Creating Windows ZIP archive..." + echo "Creating Windows ZIP archive (ZIP64 support for large files)..." echo "Archive: unilab-pack-win-64.zip" echo "Contents: install_unilab.bat + unilab-env-win-64.tar.gz + extras" - cd dist-package - powershell -Command "Compress-Archive -Path * -DestinationPath ../unilab-pack-${{ matrix.platform }}.zip -Force" - cd .. + echo "" + + # Use Python script with ZIP64 support instead of PowerShell Compress-Archive + # PowerShell Compress-Archive has a 2GB limitation + python scripts/create_zip_archive.py dist-package unilab-pack-${{ matrix.platform }}.zip else echo "Creating Unix/Linux TAR.GZ archive..." echo "Archive: unilab-pack-${{ matrix.platform }}.tar.gz" diff --git a/scripts/create_zip_archive.py b/scripts/create_zip_archive.py new file mode 100644 index 00000000..96cf6e9c --- /dev/null +++ b/scripts/create_zip_archive.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +Create ZIP Archive with ZIP64 Support +====================================== + +This script creates a ZIP archive with ZIP64 support for large files (>2GB). +It's used in the conda-pack build workflow to package the distribution. + +PowerShell's Compress-Archive has a 2GB limitation, so we use Python's zipfile +module with allowZip64=True to handle large conda-packed environments. + +Usage: + python create_zip_archive.py [--compression-level LEVEL] + +Arguments: + source_dir: Directory to compress + output_zip: Output ZIP file path + --compression-level: Compression level (0-9, default: 6) + +Example: + python create_zip_archive.py dist-package unilab-pack-win-64.zip +""" + +import argparse +import os +import sys +import zipfile +from pathlib import Path + + +def create_zip_archive(source_dir: str, output_zip: str, compression_level: int = 6) -> bool: + """ + Create a ZIP archive with ZIP64 support. + + Args: + source_dir: Directory to compress + output_zip: Output ZIP file path + compression_level: Compression level (0-9) + + Returns: + bool: True if successful + """ + try: + source_path = Path(source_dir) + output_path = Path(output_zip) + + # Validate source directory + if not source_path.exists(): + print(f"Error: Source directory does not exist: {source_dir}", file=sys.stderr) + return False + + if not source_path.is_dir(): + print(f"Error: Source path is not a directory: {source_dir}", file=sys.stderr) + return False + + # Remove existing output file if present + if output_path.exists(): + print(f"Removing existing archive: {output_path}") + output_path.unlink() + + # Create ZIP archive + print("=" * 70) + print(f"Creating ZIP archive with ZIP64 support") + print(f" Source: {source_path.absolute()}") + print(f" Output: {output_path.absolute()}") + print(f" Compression: Level {compression_level}") + print("=" * 70) + + total_size = 0 + file_count = 0 + + with zipfile.ZipFile( + output_path, "w", zipfile.ZIP_DEFLATED, allowZip64=True, compresslevel=compression_level + ) as zipf: + # Walk through source directory + for root, dirs, files in os.walk(source_dir): + for file in files: + file_path = os.path.join(root, file) + arcname = os.path.relpath(file_path, source_dir) + file_size = os.path.getsize(file_path) + + # Add file to archive + zipf.write(file_path, arcname) + + # Display progress + total_size += file_size + file_count += 1 + print(f" [{file_count:3d}] Adding: {arcname:50s} {file_size:>15,} bytes") + + # Get final archive size + archive_size = output_path.stat().st_size + compression_ratio = (1 - archive_size / total_size) * 100 if total_size > 0 else 0 + + # Display summary + print("=" * 70) + print("Archive created successfully!") + print(f" Files added: {file_count}") + print(f" Total size (uncompressed): {total_size:>15,} bytes ({total_size / (1024**3):.2f} GB)") + print(f" Archive size (compressed): {archive_size:>15,} bytes ({archive_size / (1024**3):.2f} GB)") + print(f" Compression ratio: {compression_ratio:.1f}%") + print("=" * 70) + + return True + + except Exception as e: + print(f"Error creating ZIP archive: {e}", file=sys.stderr) + import traceback + + traceback.print_exc() + return False + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Create ZIP archive with ZIP64 support for large files", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python create_zip_archive.py dist-package unilab-pack-win-64.zip + python create_zip_archive.py dist-package unilab-pack-win-64.zip --compression-level 9 + """, + ) + + parser.add_argument("source_dir", help="Directory to compress") + + parser.add_argument("output_zip", help="Output ZIP file path") + + parser.add_argument( + "--compression-level", + type=int, + default=6, + choices=range(0, 10), + metavar="LEVEL", + help="Compression level (0=no compression, 9=maximum compression, default=6)", + ) + + args = parser.parse_args() + + # Create archive + success = create_zip_archive(args.source_dir, args.output_zip, args.compression_level) + + # Exit with appropriate code + sys.exit(0 if success else 1) + + +if __name__ == "__main__": + main() diff --git a/scripts/verify_installation.py b/scripts/verify_installation.py index 92074b5c..eb6d119b 100644 --- a/scripts/verify_installation.py +++ b/scripts/verify_installation.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- """ UniLabOS Installation Verification Script ========================================= @@ -17,6 +18,10 @@ Usage: import sys import importlib +# Use ASCII-safe symbols that work across all platforms +CHECK_MARK = "[OK]" +CROSS_MARK = "[FAIL]" + def check_package(package_name: str, display_name: str = None) -> bool: """ @@ -34,10 +39,10 @@ def check_package(package_name: str, display_name: str = None) -> bool: try: importlib.import_module(package_name) - print(f" ✓ {display_name}") + print(f" {CHECK_MARK} {display_name}") return True except ImportError: - print(f" ✗ {display_name}") + print(f" {CROSS_MARK} {display_name}") return False @@ -47,10 +52,10 @@ def check_python_version() -> bool: version_str = f"{version.major}.{version.minor}.{version.micro}" if version.major == 3 and version.minor >= 11: - print(f" ✓ Python {version_str}") + print(f" {CHECK_MARK} Python {version_str}") return True else: - print(f" ✗ Python {version_str} (requires Python 3.8+)") + print(f" {CROSS_MARK} Python {version_str} (requires Python 3.8+)") return False @@ -80,23 +85,23 @@ def main(): try: from unilabos.utils.environment_check import EnvironmentChecker - print(" ✓ UniLabOS installed") + print(f" {CHECK_MARK} UniLabOS installed") checker = EnvironmentChecker() env_check_passed = checker.check_all_packages() if env_check_passed: - print(" ✓ All required packages available") + print(f" {CHECK_MARK} All required packages available") else: - print(f" ✗ Missing {len(checker.missing_packages)} package(s):") + print(f" {CROSS_MARK} Missing {len(checker.missing_packages)} package(s):") for import_name, _ in checker.missing_packages: print(f" - {import_name}") all_passed = False except ImportError: - print(" ✗ UniLabOS not installed") + print(f" {CROSS_MARK} UniLabOS not installed") all_passed = False except Exception as e: - print(f" ✗ Environment check failed: {str(e)}") + print(f" {CROSS_MARK} Environment check failed: {str(e)}") all_passed = False print() @@ -106,14 +111,14 @@ def main(): print("=" * 60) if all_passed: - print("\n✓ All checks passed! Your UniLabOS installation is ready.") + print(f"\n{CHECK_MARK} All checks passed! Your UniLabOS installation is ready.") print("\nNext steps:") print(" 1. Review the documentation: docs/user_guide/launch.md") print(" 2. Try the examples: docs/boot_examples/") print(" 3. Configure your devices: unilabos_data/startup_config.json") return 0 else: - print("\n✗ Some checks failed. Please review the errors above.") + print(f"\n{CROSS_MARK} Some checks failed. Please review the errors above.") print("\nTroubleshooting:") print(" 1. Ensure you're in the correct conda environment: conda activate unilab") print(" 2. Check the installation documentation: docs/user_guide/installation.md") diff --git a/unilabos/__init__.py b/unilabos/__init__.py index e69de29b..daecfa51 100644 --- a/unilabos/__init__.py +++ b/unilabos/__init__.py @@ -0,0 +1 @@ +__version__ = "0.10.6"