Chandra

 view release on metacpan or  search on metacpan

lib/Chandra/Pack.pm  view on Meta::CPAN


    # Info.plist
    file_spew(file_join($contents, 'Info.plist'), $self->_generate_plist());

    # Launcher (compiled C binary so macOS Launch Services accepts it)
    my $launcher = file_join($macos, lc($safe));
    $self->_compile_launcher_macos($launcher);

    # Copy script
    file_copy($self->{script}, file_join($resources, 'script.pl'));

    # Copy deps
    $self->_copy_deps($lib_dir);

    # Icon
    if ($self->{icon} && file_exists($self->{icon})) {
        my $ext = file_extname($self->{icon});
        if ($ext eq '.icns') {
            file_copy($self->{icon}, file_join($resources, 'app.icns'));
        } elsif ($ext eq '.png') {
            $self->_convert_icon_macos($self->{icon}, file_join($resources, 'app.icns'));
        }
    }

    # Assets
    $self->_copy_assets(file_join($resources, 'assets')) if $self->{assets};

    my $result = {
        success  => 1,
        path     => $app_dir,
        platform => 'macos',
        size     => _dir_size($app_dir),
    };

    # Distribution pipeline: codesign → notarize → DMG
    if ($self->{distribute}) {
        $result = $self->_distribute_macos($result);
    }

    return $result;
}

sub build_linux {
    my ($self) = @_;
    my $safe = _safe_name($self->{name});
    my $app_dir = file_join($self->{output}, $safe);
    my $usr = file_join($app_dir, 'usr');
    my $lib_dir = file_join($usr, 'lib', 'perl5');
    my $share = file_join($usr, 'share');

    # Create structure
    file_mkpath($lib_dir);
    file_mkpath($share);

    # AppRun
    my $apprun = file_join($app_dir, 'AppRun');
    file_spew($apprun, $self->_generate_launcher_linux());
    chmod 0755, $apprun;

    # Desktop entry
    file_spew(file_join($app_dir, lc($safe) . '.desktop'), $self->_generate_desktop());

    # Copy script
    file_copy($self->{script}, file_join($usr, 'share', 'script.pl'));

    # Copy deps
    $self->_copy_deps($lib_dir);

    # Icon
    if ($self->{icon} && file_exists($self->{icon})) {
        file_copy($self->{icon}, file_join($app_dir, lc($safe) . file_extname($self->{icon})));
    }

    # Assets
    $self->_copy_assets(file_join($share, 'assets')) if $self->{assets};

    my $result = {
        success  => 1,
        path     => $app_dir,
        platform => 'linux',
        size     => _dir_size($app_dir),
    };

    # Distribution pipeline: AppImage
    if ($self->{distribute}) {
        $result = $self->_distribute_linux($result);
    }

    return $result;
}

sub build_windows {
    my ($self) = @_;
    my $safe = _safe_name($self->{name});
    my $app_dir = file_join($self->{output}, $safe);
    my $lib_dir = file_join($app_dir, 'lib');

    # Create structure
    file_mkpath($lib_dir);

    # Launcher bat
    file_spew(file_join($app_dir, lc($safe) . '.bat'), $self->_generate_launcher_windows());

    # Copy script
    file_copy($self->{script}, file_join($app_dir, 'script.pl'));

    # Copy deps
    $self->_copy_deps($lib_dir);

    # Assets
    $self->_copy_assets(file_join($app_dir, 'assets')) if $self->{assets};

    # Authenticode signing if cert provided
    if ($self->{sign_cert}) {
        my $exe = file_join($app_dir, lc($safe) . '.bat');
        my $cert = $self->{sign_cert};
        my $pass = $self->{sign_password};
        my @cmd = ('signtool', 'sign', '/f', $cert, '/fd', 'SHA256', '/tr',
                   'http://timestamp.digicert.com', '/td', 'SHA256');
        push @cmd, '/p', $pass if $pass;
        push @cmd, $exe;

lib/Chandra/Pack.pm  view on Meta::CPAN

    my ($self, $app_path) = @_;
    
    my $safe = _safe_name($self->{name});
    my $vol_name = $self->{name};
    my $dmg_path = file_join($self->{output}, "$safe.dmg");
    my $temp_dmg = "$dmg_path.tmp";
    
    # Create temporary DMG
    my $size = _dir_size($app_path);
    my $size_mb = int($size / 1_000_000) + 50;  # Add 50MB headroom
    
    # Create DMG
    my @hdiutil = (
        'hdiutil', 'create',
        '-volname', $vol_name,
        '-srcfolder', $app_path,
        '-ov',
        '-format', 'UDBZ',  # Compressed
        $dmg_path
    );
    
    my $output = `@hdiutil 2>&1`;
    if ($? != 0) {
        return { success => 0, error => "DMG creation failed: $output" };
    }
    
    return { success => 1, path => $dmg_path };
}

sub _distribute_linux {
    my ($self, $result) = @_;
    my $app_dir = $result->{path};
    
    # Check for appimagetool
    my $appimagetool = _find_command('appimagetool');
    unless ($appimagetool) {
        Carp::carp("appimagetool not found; skipping AppImage creation");
        return $result;
    }
    
    # Create AppImage
    my $appimage_result = $self->_create_appimage($app_dir, $appimagetool);
    unless ($appimage_result->{success}) {
        return { %$result, success => 0, error => $appimage_result->{error} };
    }
    
    $result->{appimage_path} = $appimage_result->{path};
    $result->{size} = file_size($appimage_result->{path});
    
    return $result;
}

sub _create_appimage {
    my ($self, $app_dir, $appimagetool) = @_;
    
    my $safe = _safe_name($self->{name});
    my $appimage_path = file_join($self->{output}, "$safe.AppImage");
    
    # Ensure proper AppDir structure
    # AppRun should already exist from build_linux
    # Ensure .desktop file is at root level
    my $desktop_file = file_join($app_dir, lc($safe) . '.desktop');
    unless (file_is_file($desktop_file)) {
        return { success => 0, error => "Missing .desktop file" };
    }
    
    # Run appimagetool
    my $cmd = "\Q$appimagetool\E \Q$app_dir\E \Q$appimage_path\E 2>&1";
    my $output = `$cmd`;
    if ($? != 0) {
        return { success => 0, error => "appimagetool failed: $output" };
    }
    
    # Make executable
    chmod 0755, $appimage_path;
    
    return { success => 1, path => $appimage_path };
}

sub _find_command {
    my ($cmd) = @_;
    my $path = `which $cmd 2>/dev/null`;
    chomp $path;
    return $path if $path && -x $path;
    
    # Check common locations
    for my $dir ('/usr/local/bin', '/usr/bin', "$ENV{HOME}/bin", "$ENV{HOME}/.local/bin") {
        my $full = "$dir/$cmd";
        return $full if -x $full;
    }
    
    return undef;
}

# ── Template Generators ──────────────────────────────────────────────

sub _generate_plist {
    my ($self) = @_;
    my $safe = _safe_name($self->{name});
    my $has_icon = ($self->{icon} && file_exists($self->{icon})) ? 1 : 0;
    my $plist = <<PLIST;
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleName</key><string>$self->{name}</string>
    <key>CFBundleIdentifier</key><string>$self->{identifier}</string>
    <key>CFBundleVersion</key><string>$self->{version}</string>
    <key>CFBundleShortVersionString</key><string>$self->{version}</string>
    <key>CFBundleExecutable</key><string>${\ lc($safe) }</string>
    <key>CFBundlePackageType</key><string>APPL</string>
    <key>NSHighResolutionCapable</key><true/>
    <key>LSEnvironment</key>
    <dict>
        <key>PATH</key><string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
    </dict>
PLIST
    if ($has_icon) {
        $plist .= "    <key>CFBundleIconFile</key><string>app</string>\n";
    }
    $plist .= "</dict>\n</plist>\n";
    return $plist;
}

lib/Chandra/Pack.pm  view on Meta::CPAN

    const char *existing = getenv("PERL5LIB");
    if (existing && *existing)
        snprintf(perl5lib, sizeof(perl5lib), "%s:%s", lib, existing);
    else
        snprintf(perl5lib, sizeof(perl5lib), "%s", lib);
    setenv("PERL5LIB", perl5lib, 1);

CSRC
    $c_src .= "    execl(\"$full_perl\", \"$full_perl\", script, (char *)NULL);\n";
    $c_src .= <<'CSRC';
    perror("exec");
    return 1;
}
CSRC

    require File::Temp;
    my $tmp = File::Temp->new(SUFFIX => '.c', UNLINK => 1);
    print $tmp $c_src;
    close $tmp;

    my $cmd = "cc -o \Q$output\E -framework Foundation \Q$tmp\E 2>&1";
    my $out = `$cmd`;
    if ($? != 0) {
        die "Failed to compile macOS launcher: $out\n";
    }
}

sub _generate_launcher_macos {
    my ($self) = @_;
    my $perl = $self->{perl};
    return <<'LAUNCHER';
#!/bin/bash
DIR="$(cd "$(dirname "$0")" && pwd)"
export PERL5LIB="$DIR/../Resources/lib:$PERL5LIB"
exec "$perl" "$DIR/../Resources/script.pl" "$@"
LAUNCHER
}

sub _generate_launcher_linux {
    my ($self) = @_;
    my $perl = $self->{perl};
    return <<LAUNCHER;
#!/bin/bash
DIR="\$(cd "\$(dirname "\$0")" && pwd)"
export PERL5LIB="\$DIR/usr/lib/perl5:\$PERL5LIB"
exec "$perl" "\$DIR/usr/share/script.pl" "\$@"
LAUNCHER
}

sub _generate_launcher_windows {
    my ($self) = @_;
    my $perl = $self->{perl};
    return <<LAUNCHER;
\@echo off
set DIR=%~dp0
set PERL5LIB=%DIR%lib;%PERL5LIB%
"$perl" "%DIR%script.pl" %*
LAUNCHER
}

sub _generate_desktop {
    my ($self) = @_;
    my $safe = lc(_safe_name($self->{name}));
    my $icon_ext = '';
    $icon_ext = file_extname($self->{icon}) if $self->{icon} && file_exists($self->{icon});
    return <<DESKTOP;
[Desktop Entry]
Type=Application
Name=$self->{name}
Exec=./AppRun
Icon=${safe}${icon_ext}
Categories=Utility;
DESKTOP
}

# ── Internal Helpers ─────────────────────────────────────────────────

sub _copy_deps {
    my ($self, $lib_dir) = @_;
    my @deps = $self->scan_deps;
    for my $dep (@deps) {
        my $mod_file = _mod_to_file($dep->{module});
        my $dest = file_join($lib_dir, $mod_file);
        my $dest_dir = file_dirname($dest);
        file_mkpath($dest_dir) unless file_is_dir($dest_dir);
        file_copy($dep->{file}, $dest);

        # Copy XS shared object if present
        my $xs_so = _find_xs_so($dep->{file}, $dep->{module});
        if ($xs_so) {
            my $auto_dir = _auto_dir($dep->{module});
            my $so_dest_dir = file_join($lib_dir, $auto_dir);
            file_mkpath($so_dest_dir) unless file_is_dir($so_dest_dir);
            file_copy($xs_so, file_join($so_dest_dir, file_basename($xs_so)));
        }
    }
}

sub _copy_assets {
    my ($self, $dest_dir) = @_;
    my $src = $self->{assets};
    return unless $src && file_is_dir($src);
    _copy_dir_recursive($src, $dest_dir);
}

sub _copy_dir_recursive {
    my ($src, $dest) = @_;
    file_mkpath($dest) unless file_is_dir($dest);
    my $entries = file_readdir($src);
    for my $entry (@$entries) {
        next if $entry eq '.' || $entry eq '..';
        my $s = file_join($src, $entry);
        my $d = file_join($dest, $entry);
        if (file_is_dir($s)) {
            _copy_dir_recursive($s, $d);
        } else {
            file_copy($s, $d);
        }
    }
}



( run in 0.800 second using v1.01-cache-2.11-cpan-df04353d9ac )