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 )