Chandra
view release on metacpan or search on metacpan
lib/Chandra/Pack.pm view on Meta::CPAN
}
# Setter: config(key => val, ...)
my %args = ref $_[0] eq 'HASH' ? %{$_[0]} : @_;
# Load from env vars if not provided
$args{identity} //= $ENV{CHANDRA_IDENTITY};
$args{apple_id} //= $ENV{CHANDRA_APPLE_ID};
$args{team_id} //= $ENV{CHANDRA_TEAM_ID};
$args{notary_keychain} //= $ENV{CHANDRA_NOTARY_KEYCHAIN};
$args{sign_cert} //= $ENV{CHANDRA_SIGN_CERT};
$args{sign_password} //= $ENV{CHANDRA_SIGN_PASSWORD};
for my $key (keys %args) {
if (exists $CONFIG{$key}) {
$CONFIG{$key} = $args{$key};
} else {
Carp::carp("Unknown config key: $key");
}
}
# Save to file if requested
if (delete $args{save}) {
_save_config();
}
return $class;
}
sub _config_file {
my $home = $ENV{HOME};
if (!$home && $^O ne 'MSWin32') {
$home = (getpwuid($<))[7];
}
$home ||= $ENV{USERPROFILE} || '.';
return file_join($home, '.chandra', 'pack.conf');
}
sub _load_config {
my $file = _config_file();
return unless file_is_file($file);
my $content = file_slurp($file);
for my $line (split /\n/, $content) {
next if $line =~ /^\s*#/ || $line =~ /^\s*$/;
if ($line =~ /^(\w+)\s*=\s*(.*)$/) {
$CONFIG{$1} = $2 if exists $CONFIG{$1};
}
}
}
sub _save_config {
my $file = _config_file();
my $dir = file_dirname($file);
file_mkpath($dir) unless file_is_dir($dir);
my $content = "# Chandra::Pack configuration\n";
for my $key (sort keys %CONFIG) {
next unless defined $CONFIG{$key};
$content .= "$key = $CONFIG{$key}\n";
}
file_spew($file, $content);
chmod 0600, $file; # Protect credentials
}
# Load config on module load
_load_config();
sub new {
my ($class, %args) = @_;
Carp::croak("'script' is required") unless $args{script};
Carp::croak("Script '$args{script}' not found") unless file_exists($args{script});
Carp::croak("Script '$args{script}' is not a file") unless file_is_file($args{script});
my $name = $args{name} || _name_from_script($args{script});
bless {
script => Cwd::abs_path($args{script}),
name => $name,
version => $args{version} || '0.0.1',
icon => $args{icon},
assets => $args{assets},
output => $args{output} || '.',
platform => $args{platform} || _detect_platform(),
identifier => $args{identifier} || _default_identifier($name),
perl => $args{perl} || $^X,
include => $args{include} || [],
exclude => $args{exclude} || [],
distribute => $args{distribute} || 0,
_deps => undef,
}, $class;
}
# ââ Accessors ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
sub script { $_[0]->{script} }
sub name { $_[0]->{name} }
sub version { $_[0]->{version} }
sub icon { $_[0]->{icon} }
sub assets { $_[0]->{assets} }
sub output { $_[0]->{output} }
sub platform { $_[0]->{platform} }
sub identifier { $_[0]->{identifier} }
sub perl { $_[0]->{perl} }
sub distribute { $_[0]->{distribute} }
# ââ Dependency Scanning ââââââââââââââââââââââââââââââââââââââââââââââ
sub scan_deps {
my ($self) = @_;
return @{ $self->{_deps} } if $self->{_deps};
my %seen;
my %exclude = map { $_ => 1 } @{ $self->{exclude} };
my @queue = ($self->{script});
my @deps;
# Add explicit includes
for my $mod (@{ $self->{include} }) {
my $file = _mod_to_file($mod);
my $path = _find_in_inc($file);
if ($path) {
push @deps, { module => $mod, file => $path };
push @queue, $path;
lib/Chandra/Pack.pm view on Meta::CPAN
return 1 if $mod =~ /^(strict|warnings|utf8|constant|feature|overload|overloading|lib|vars|base|parent|fields|integer|bigint|bignum|bigrat|bytes|charnames|diagnostics|encoding|if|less|locale|mro|open|ops|re|sigtrap|sort|subs|threads|threads::shar...
return 1 if $mod =~ /^[a-z]/; # convention: pragmas are lowercase
return 0;
}
sub _name_from_script {
my ($script) = @_;
my $base = file_basename($script);
$base =~ s/\.pl$//;
$base =~ s/[_-]/ /g;
return ucfirst($base);
}
sub _safe_name {
my ($name) = @_;
$name =~ s/[^a-zA-Z0-9_]//g;
return $name || 'App';
}
sub _default_identifier {
my ($name) = @_;
my $safe = lc(_safe_name($name));
return "org.perl.$safe";
}
sub _detect_platform {
return 'macos' if $^O eq 'darwin';
return 'windows' if $^O eq 'MSWin32';
return 'linux';
}
sub _dir_size {
my ($dir) = @_;
return 0 unless file_is_dir($dir);
my $total = 0;
my $entries = file_readdir($dir);
for my $e (@$entries) {
next if $e eq '.' || $e eq '..';
my $path = file_join($dir, $e);
if (file_is_dir($path)) {
$total += _dir_size($path);
} else {
$total += file_size($path);
}
}
return $total;
}
1;
__END__
=head1 NAME
Chandra::Pack - Bundle Chandra apps into distributable packages
=head1 SYNOPSIS
use Chandra::Pack;
# Configure credentials (once, persists to ~/.chandra/pack.conf)
Chandra::Pack->config(
identity => 'Developer ID Application: Your Name',
apple_id => 'you@example.com',
team_id => 'ABCD1234',
save => 1,
);
my $packer = Chandra::Pack->new(
script => 'app.pl',
name => 'My App',
version => '1.0.0',
icon => 'icon.png',
assets => 'assets/',
output => 'dist/',
identifier => 'com.example.myapp',
distribute => 1, # Full release pipeline
);
# Build with distribution (sign, notarize, DMG on macOS; AppImage on Linux)
$packer->build(sub {
my ($result) = @_;
print "Built: $result->{path}\n" if $result->{success};
print "DMG: $result->{dmg_path}\n" if $result->{dmg_path};
});
=head1 DESCRIPTION
Chandra::Pack bundles a Perl script and its dependencies into a
distributable application package. It creates C<.app> bundles on macOS,
AppImage-style directories on Linux, and portable directories on Windows.
When C<distribute =E<gt> 1> is set, the full release pipeline runs:
=over 4
=item * B<macOS>: codesign â notarize â staple â DMG
=item * B<Linux>: AppImage (via appimagetool)
=item * B<Windows>: Directory build (installer support planned)
=back
=head1 CLASS METHODS
=head2 config(%args)
Configure credentials for signing and notarization. Call once before
building. Settings can be persisted to C<~/.chandra/pack.conf>.
Chandra::Pack->config(
# macOS signing/notarization
identity => 'Developer ID Application: ...',
apple_id => 'your@email.com',
team_id => 'ABCD1234',
notary_keychain => 'notary-profile', # from notarytool store-credentials
# Persist to disk
save => 1,
);
Environment variables are used as fallbacks:
CHANDRA_IDENTITY
CHANDRA_APPLE_ID
CHANDRA_TEAM_ID
CHANDRA_NOTARY_KEYCHAIN
CHANDRA_NOTARY_PASSWORD
=head1 INSTANCE METHODS
=head2 new(%args)
my $packer = Chandra::Pack->new(
script => 'app.pl', # required
name => 'My App', # default: derived from script name
version => '1.0.0', # default: 0.0.1
icon => 'icon.png', # optional
assets => 'assets/', # optional
output => 'dist/', # default: .
platform => 'macos', # default: current platform
identifier => 'com.x.myapp', # default: org.perl.<name>
perl => '/usr/bin/perl',# default: current perl
include => ['DBI'], # extra modules to include
exclude => ['Test::More'], # modules to skip
distribute => 1, # run full distribution pipeline
);
=head2 scan_deps
Returns a list of hashrefs with C<module> and C<file> keys for all
detected dependencies.
=head2 build($callback)
Build for the configured platform. Calls C<$callback> with a result
hashref containing C<success>, C<path>, C<platform>, and C<size>.
When C<distribute =E<gt> 1>:
=over 4
=item * macOS: adds C<signed>, C<notarized>, C<dmg_path>
=item * Linux: adds C<appimage_path>
=back
=head2 build_macos
Build a macOS C<.app> bundle. With C<distribute =E<gt> 1>, also signs,
notarizes, and creates a DMG.
=head2 build_linux
Build a Linux AppImage-style directory. With C<distribute =E<gt> 1>,
runs appimagetool to create a standalone AppImage.
=head2 build_windows
Build a Windows portable directory.
=head1 DISTRIBUTION DETAILS
=head2 macOS Code Signing
Uses hardened runtime with entitlements for JIT and unsigned memory
(required for Perl). Ad-hoc signing (C<identity =E<gt> '-'>) skips
notarization but still works locally.
=head2 macOS Notarization
Requires Apple Developer account. Store credentials once with:
xcrun notarytool store-credentials notary-profile \
--apple-id your@email.com \
--team-id ABCD1234 \
--password your-app-specific-password
Then configure:
Chandra::Pack->config(notary_keychain => 'notary-profile');
=head2 Linux AppImage
Requires C<appimagetool> in PATH. Install from:
L<https://github.com/AppImage/AppImageKit/releases>
=head1 SEE ALSO
L<Chandra::App>
=cut
( run in 1.112 second using v1.01-cache-2.11-cpan-140bd7fdf52 )