clanker 1 mēnesi atpakaļ
vecāks
revīzija
63a58f0885
5 mainītis faili ar 368 papildinājumiem un 41 dzēšanām
  1. 107 29
      src/autusm/cli.py
  2. 24 11
      src/autusm/generator.py
  3. 20 1
      src/autusm/manifest.py
  4. 69 0
      src/autusm/models.py
  5. 148 0
      test_execs_fix.py

+ 107 - 29
src/autusm/cli.py

@@ -9,8 +9,9 @@ import os
 import sys
 import logging
 import shutil
+import urllib.parse
 from pathlib import Path
-from typing import Optional
+from typing import Optional, Tuple
 
 import click
 
@@ -35,12 +36,12 @@ logger = logging.getLogger(__name__)
 
 
 @click.command()
-@click.argument("url", required=True)
+@click.argument("source", required=True)
 @click.option(
     "--output-dir", "-o",
     type=click.Path(exists=False, file_okay=False, dir_okay=True),
-    default=".",
-    help="Output directory for generated files (default: current directory)"
+    default=None,
+    help="Output directory for generated files (default: same as source for local directories)"
 )
 @click.option(
     "--work-dir", "-w",
@@ -87,11 +88,11 @@ logger = logging.getLogger(__name__)
     "--clean-source",
     is_flag=True,
     default=False,
-    help="Clean up extracted source files after package creation"
+    help="Clean up extracted source files after package creation (not valid for local directories)"
 )
 def main(
-    url: str,
-    output_dir: str,
+    source: str,
+    output_dir: Optional[str],
     work_dir: Optional[str],
     name: Optional[str],
     version: Optional[str],
@@ -103,9 +104,10 @@ def main(
     clean_source: bool
 ) -> None:
     """
-    Generate USM manifest from source archive URL.
+    Generate USM manifest from source archive URL or local directory.
     
-    URL is the URL to a source archive (tar, tar.gz, tar.bz2, zip).
+    SOURCE is either a URL to a source archive (tar, tar.gz, tar.bz2, zip)
+    or a path to a local directory containing source code.
     """
     # Configure logging level
     if verbose:
@@ -114,6 +116,14 @@ def main(
         logging.getLogger().setLevel(logging.ERROR)
 
     try:
+        # Check if source is a URL or local directory
+        is_url, source_path = _detect_source_type(source)
+        
+        # Validate --clean-source flag for local directories
+        if not is_url and clean_source:
+            logger.error("--clean-source flag is not valid when processing local directories")
+            sys.exit(1)
+        
         # Initialize components
         download_manager = DownloadManager()
         extractor = ArchiveExtractor()
@@ -124,30 +134,56 @@ def main(
         usm_integration = USMIntegration()
         user_interaction = UserInteraction(interactive=not non_interactive)
 
-        # Set up working directory
-        if work_dir:
-            work_path = Path(work_dir)
-            work_path.mkdir(parents=True, exist_ok=True)
+        # Set up working directory (only needed for URLs)
+        if is_url:
+            if work_dir:
+                work_path = Path(work_dir)
+                work_path.mkdir(parents=True, exist_ok=True)
+            else:
+                import tempfile
+                work_path = Path(tempfile.mkdtemp(prefix="autusm-"))
+            logger.info(f"Using working directory: {work_path}")
         else:
-            import tempfile
-            work_path = Path(tempfile.mkdtemp(prefix="autusm-"))
-
-        logger.info(f"Using working directory: {work_path}")
+            work_path = None
 
         # Set up output directory
-        output_path = Path(output_dir)
+        if is_url:
+            # For URLs, use the specified output directory or default to current
+            output_path = Path(output_dir or ".")
+        else:
+            # For local directories, use the local directory itself as output
+            output_path = source_path
+            # Override output_dir if it was specified (but warn user)
+            if output_dir:
+                logger.warning("Output directory is ignored when processing local directories")
+        
         output_path.mkdir(parents=True, exist_ok=True)
         logger.info(f"Using output directory: {output_path}")
 
-        # Step 1: Download the source archive
-        logger.info(f"Downloading source archive from: {url}")
-        archive_path = download_manager.download(url, work_path)
-        logger.info(f"Downloaded to: {archive_path}")
+        # Check for existing MANIFEST.usm in local directory mode
+        if not is_url:
+            manifest_path = output_path / "MANIFEST.usm"
+            if manifest_path.exists():
+                logger.error(f"MANIFEST.usm already exists in {output_path}")
+                sys.exit(1)
+
+        # Get source directory and extract metadata
+        if is_url:
+            # URL workflow
+            logger.info(f"Downloading source archive from: {source}")
+            archive_path = download_manager.download(source, work_path)
+            logger.info(f"Downloaded to: {archive_path}")
 
-        # Step 2: Extract the archive directly to the output directory
-        logger.info("Extracting archive to output directory...")
-        source_dir = extractor.extract(archive_path, output_path)
-        logger.info(f"Extracted to: {source_dir}")
+            logger.info("Extracting archive to output directory...")
+            source_dir = extractor.extract(archive_path, output_path)
+            logger.info(f"Extracted to: {source_dir}")
+            
+            url = source
+        else:
+            # Local directory workflow
+            logger.info(f"Using local source directory: {source_path}")
+            source_dir = source_path
+            url = ""
 
         # Step 3: Analyze the source code
         logger.info("Analyzing source code...")
@@ -181,10 +217,23 @@ def main(
         scripts_dir = output_path / "scripts"
         scripts_dir.mkdir(parents=True, exist_ok=True)
         
+        # For local directories, check if scripts already exist
+        if not is_url:
+            existing_scripts = []
+            for script_name in ["build", "install"]:
+                script_path = scripts_dir / script_name
+                if script_path.exists():
+                    existing_scripts.append(script_name)
+            
+            if existing_scripts:
+                logger.info(f"Skipping generation of existing scripts: {', '.join(existing_scripts)}")
+        
+        # Generate scripts (will skip acquire script for local directories)
         script_generator.generate_scripts(
             package_info,
             build_system,
-            scripts_dir
+            scripts_dir,
+            skip_acquire=not is_url  # Skip acquire script for local directories
         )
 
         # Step 7: Generate USM manifest
@@ -222,8 +271,8 @@ def main(
             else:
                 logger.warning("USM is not available, skipping autoprovides")
         
-        # Step 10: Clean up source files if requested
-        if clean_source:
+        # Step 10: Clean up source files if requested (only for URLs)
+        if clean_source and is_url:
             _cleanup_source_files(source_dir, output_path, logger)
         
         # Print summary
@@ -303,5 +352,34 @@ def _cleanup_source_files(source_dir: Path, output_path: Path, logger) -> None:
         # Don't raise the exception - cleanup failure shouldn't break the whole process
 
 
+def _detect_source_type(source: str) -> Tuple[bool, Path]:
+    """
+    Detect if the source argument is a URL or a local directory.
+    
+    Args:
+        source: The source argument from command line
+        
+    Returns:
+        Tuple of (is_url, path)
+        - is_url: True if source is a URL, False if it's a local directory
+        - path: Path object for the source (URL for is_url=True, directory path for is_url=False)
+    """
+    # Check if it's a URL
+    parsed = urllib.parse.urlparse(source)
+    if parsed.scheme and parsed.netloc:
+        # It's a URL
+        return True, Path(source)
+    
+    # Check if it's a local directory
+    path = Path(source)
+    if path.exists() and path.is_dir():
+        # It's a local directory
+        return False, path.resolve()
+    
+    # If it's not a URL and not an existing directory, assume it's a URL
+    # This maintains backward compatibility
+    return True, Path(source)
+
+
 if __name__ == "__main__":
     main()

+ 24 - 11
src/autusm/generator.py

@@ -217,13 +217,14 @@ echo "Source directory verified: $SOURCE_DIR"
 echo "Source acquired for {package_info.name}"
 """
 
-    def generate_scripts(self, package_info: PackageInfo, build_system: BuildSystem, output_dir: Path) -> None:
+    def generate_scripts(self, package_info: PackageInfo, build_system: BuildSystem, output_dir: Path, skip_acquire: bool = False) -> None:
         """Generate USM scripts for a package.
         
         Args:
             package_info: Package information
             build_system: Detected build system
             output_dir: Directory to write scripts to
+            skip_acquire: Whether to skip generating the acquire script (for local directories)
             
         Raises:
             ScriptGenerationError: If script generation fails
@@ -240,20 +241,32 @@ echo "Source acquired for {package_info.name}"
                 self.script_templates[BuildSystemType.UNKNOWN]
             )
             
-            # Generate acquire script
-            acquire_content = templates["acquire"](package_info, build_system)
-            acquire_path = output_dir / "acquire"
-            self._write_script(acquire_path, acquire_content)
+            # Generate acquire script (unless skipped)
+            if not skip_acquire:
+                acquire_content = templates["acquire"](package_info, build_system)
+                acquire_path = output_dir / "acquire"
+                self._write_script(acquire_path, acquire_content)
+                logger.info("Generated acquire script")
+            else:
+                logger.info("Skipping acquire script generation for local directory")
             
-            # Generate build script
-            build_content = templates["build"](package_info, build_system)
+            # Generate build script (only if it doesn't already exist)
             build_path = output_dir / "build"
-            self._write_script(build_path, build_content)
+            if not build_path.exists():
+                build_content = templates["build"](package_info, build_system)
+                self._write_script(build_path, build_content)
+                logger.info("Generated build script")
+            else:
+                logger.info("Skipping build script generation (already exists)")
             
-            # Generate install script
-            install_content = templates["install"](package_info, build_system)
+            # Generate install script (only if it doesn't already exist)
             install_path = output_dir / "install"
-            self._write_script(install_path, install_content)
+            if not install_path.exists():
+                install_content = templates["install"](package_info, build_system)
+                self._write_script(install_path, install_content)
+                logger.info("Generated install script")
+            else:
+                logger.info("Skipping install script generation (already exists)")
             
             logger.info(f"Generated scripts in {output_dir}")
             

+ 20 - 1
src/autusm/manifest.py

@@ -63,6 +63,21 @@ class ManifestGenerator:
             if not package_info.summary:
                 raise ManifestGenerationError("Package summary is required")
             
+            # Check if we're working with a local directory and MANIFEST.usm already exists
+            if package_info.source_dir:
+                source_path = Path(package_info.source_dir)
+                manifest_path = source_path / "MANIFEST.usm"
+                if manifest_path.exists():
+                    logger.info(f"MANIFEST.usm already exists at {manifest_path}")
+                    # Read the existing manifest and return it
+                    try:
+                        with open(manifest_path, 'r') as f:
+                            content = f.read()
+                        return USMManifest.from_json(content)
+                    except Exception as e:
+                        logger.warning(f"Failed to read existing MANIFEST.usm: {e}")
+                        # Fall through to generate a new one
+            
             # Create dependencies
             dependencies = Dependencies(
                 runtime=self._convert_to_resource_refs(package_info.runtime_dependencies),
@@ -259,10 +274,14 @@ class ManifestGenerator:
         Returns:
             Executables object
         """
+        # Only include acquire script if we're processing a URL (not a local directory)
+        # Local directories have an empty URL, while URLs have a non-empty URL
+        acquire_script = "scripts/acquire" if package_info.url else None
+        
         return Executables(
             build="scripts/build",
             install="scripts/install",
-            acquire="scripts/acquire"
+            acquire=acquire_script
         )
 
     def _generate_flags(self, build_system: BuildSystem) -> List[str]:

+ 69 - 0
src/autusm/models.py

@@ -252,6 +252,75 @@ class USMManifest:
         """Convert the manifest to a JSON string."""
         return json.dumps(self.to_dict(), indent=indent)
 
+    @classmethod
+    def from_json(cls, json_str: str) -> "USMManifest":
+        """Create a USMManifest from a JSON string."""
+        data = json.loads(json_str)
+        return cls.from_dict(data)
+
+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> "USMManifest":
+        """Create a USMManifest from a dictionary."""
+        # Parse licenses
+        licenses = []
+        for lic_data in data.get("licences", []):
+            licenses.append(License(
+                name=lic_data["name"],
+                text=lic_data["text"],
+                category=LicenseCategory(lic_data["category"])
+            ))
+        
+        # Parse dependencies
+        deps_data = data.get("depends", {})
+        dependencies = Dependencies(
+            runtime=deps_data.get("runtime", []),
+            build=deps_data.get("build", []),
+            manage=deps_data.get("manage", []),
+            acquire=deps_data.get("acquire", [])
+        )
+        
+        # Parse provides
+        provides = {}
+        for key, value in data.get("provides", {}).items():
+            provides[key] = value
+        
+        # Parse executables
+        execs_data = data.get("execs", {})
+        executables = Executables(
+            build=execs_data.get("build"),
+            install=execs_data.get("install"),
+            remove=execs_data.get("remove"),
+            post_install=execs_data.get("postInstall"),
+            acquire=execs_data.get("acquire")
+        )
+        
+        # Parse git info if present
+        git_info = None
+        if "git" in data:
+            git_data = data["git"]
+            git_info = GitInfo(
+                origin=git_data["origin"],
+                commit=git_data["commit"]
+            )
+        
+        return cls(
+            name=data["name"],
+            version=data["version"],
+            summary=data["summary"],
+            licences=licenses,
+            provides=provides,
+            depends=dependencies,
+            flags=data.get("flags", []),
+            execs=executables,
+            md=data.get("md"),
+            url=data.get("url"),
+            screenshots=data.get("screenshots", []),
+            icon=data.get("icon"),
+            metainfo=data.get("metainfo"),
+            git=git_info,
+            extras=data.get("extras", {})
+        )
+
     @classmethod
     def from_package_info(cls, package_info: PackageInfo) -> "USMManifest":
         """Create a USMManifest from PackageInfo."""

+ 148 - 0
test_execs_fix.py

@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+"""
+Test script to verify that the execs object in MANIFEST.usm correctly reflects
+which scripts are actually present in the package.
+
+This tests the fix for the issue where "acquire" was always included in execs
+even when not generating an acquire script for local directories.
+"""
+
+import json
+import tempfile
+import shutil
+from pathlib import Path
+from src.autusm.models import PackageInfo, BuildSystem, BuildSystemType
+from src.autusm.manifest import ManifestGenerator
+
+
+def test_execs_for_url():
+    """Test that acquire is included in execs when processing a URL."""
+    print("Testing execs for URL...")
+    
+    # Create package info with a URL (simulating URL processing)
+    package_info = PackageInfo(
+        name="test-package",
+        version="1.0.0",
+        summary="Test package for URL",
+        url="https://example.com/test-package.tar.gz"
+    )
+    
+    # Create build system
+    build_system = BuildSystem(type=BuildSystemType.MAKE)
+    
+    # Generate manifest
+    manifest_generator = ManifestGenerator()
+    manifest = manifest_generator.generate(package_info, build_system)
+    
+    # Convert to dict to check the execs section
+    manifest_dict = manifest.to_dict()
+    
+    # Check that acquire is included in execs
+    assert "acquire" in manifest_dict["execs"], "acquire should be in execs for URLs"
+    assert manifest_dict["execs"]["acquire"] == "scripts/acquire", "acquire path should be scripts/acquire"
+    
+    print("✓ URL test passed: acquire is included in execs")
+    return True
+
+
+def test_execs_for_local_directory():
+    """Test that acquire is NOT included in execs when processing a local directory."""
+    print("Testing execs for local directory...")
+    
+    # Create package info without a URL (simulating local directory processing)
+    package_info = PackageInfo(
+        name="test-package",
+        version="1.0.0",
+        summary="Test package for local directory",
+        url=""  # Empty URL indicates local directory
+    )
+    
+    # Create build system
+    build_system = BuildSystem(type=BuildSystemType.MAKE)
+    
+    # Generate manifest
+    manifest_generator = ManifestGenerator()
+    manifest = manifest_generator.generate(package_info, build_system)
+    
+    # Convert to dict to check the execs section
+    manifest_dict = manifest.to_dict()
+    
+    # Check that acquire is NOT included in execs
+    assert "acquire" not in manifest_dict["execs"], "acquire should NOT be in execs for local directories"
+    
+    print("✓ Local directory test passed: acquire is NOT included in execs")
+    return True
+
+
+def test_manifest_json_output():
+    """Test the actual JSON output to ensure it's correct."""
+    print("Testing manifest JSON output...")
+    
+    # Create a temporary directory for testing
+    with tempfile.TemporaryDirectory() as temp_dir:
+        temp_path = Path(temp_dir)
+        
+        # Test 1: URL case
+        print("\n1. Testing URL case JSON output:")
+        url_package_info = PackageInfo(
+            name="url-test",
+            version="1.0.0",
+            summary="Test URL package",
+            url="https://example.com/url-test.tar.gz"
+        )
+        build_system = BuildSystem(type=BuildSystemType.CMAKE)
+        
+        manifest_generator = ManifestGenerator()
+        url_manifest = manifest_generator.generate(url_package_info, build_system)
+        url_manifest_dict = url_manifest.to_dict()
+        
+        print(json.dumps(url_manifest_dict["execs"], indent=2))
+        assert "acquire" in url_manifest_dict["execs"]
+        
+        # Test 2: Local directory case
+        print("\n2. Testing local directory case JSON output:")
+        local_package_info = PackageInfo(
+            name="local-test",
+            version="1.0.0",
+            summary="Test local package",
+            url=""  # Empty URL
+        )
+        
+        local_manifest = manifest_generator.generate(local_package_info, build_system)
+        local_manifest_dict = local_manifest.to_dict()
+        
+        print(json.dumps(local_manifest_dict["execs"], indent=2))
+        assert "acquire" not in local_manifest_dict["execs"]
+    
+    print("✓ JSON output test passed")
+    return True
+
+
+def main():
+    """Run all tests."""
+    print("Testing execs fix for manifest generation...\n")
+    
+    try:
+        test_execs_for_url()
+        test_execs_for_local_directory()
+        test_manifest_json_output()
+        
+        print("\n✅ All tests passed! The fix is working correctly.")
+        print("\nSummary:")
+        print("- For URLs: acquire script is included in execs")
+        print("- For local directories: acquire script is NOT included in execs")
+        
+    except AssertionError as e:
+        print(f"\n❌ Test failed: {e}")
+        return 1
+    except Exception as e:
+        print(f"\n❌ Unexpected error: {e}")
+        import traceback
+        traceback.print_exc()
+        return 1
+    
+    return 0
+
+
+if __name__ == "__main__":
+    exit(main())