diff --git a/cronify.pl b/cronify.pl index e354b44..cb9d6da 100755 --- a/cronify.pl +++ b/cronify.pl @@ -1,71 +1,55 @@ -#!/bin/env perl +#!/usr/bin/env perl +use strict; +use warnings; my @timestamps; while(<>) { - my @timestamp = ("*", "*", "*", "*", "*"); + # Default Cron: min(0) hour(1) dom(2) mon(3) dow(4) + my @timestamp = ("*", "*", "*", "*", "*"); + if(m/OnCalendar=(.*)/) { - if ($1 =~ /^(hourly|daily|monthly|yearly)$/) { - # Use cron's special strings for better readability - @timestamp = ('@' . "$1"); - } elsif ($1 =~ /minutely/) { - @timestamp = ("*", "*", "*", "*", "*"); - } elsif ($1 =~ /weekly/) { - # Systemd does weekly on Mondays, but cron - # does it on Sundays. We don't want unexpected - # behaviour, so we force cron to do it on Mondays - @timestamp = ("0", "0", "*", "*", "Mon"); - } elsif ($1 =~ /quarterly/) { - @timestamp = ("0", "0", "1,4,7,10", "1", "*"); - } elsif ($1 =~ /semianually/) { - @timestamp = ("0", "0", "1,7", "1", "*") - } else { - my @sysd_date = split / /, lc $1; + my $val = lc $1; + + if ($val =~ /^(hourly|daily|monthly|yearly)$/) { + @timestamp = ('@' . $val); + } elsif ($val =~ /weekly/) { + # Systemd weekly = Mon 00:00. Cron weekly = Sun 00:00. + # We force Mon for consistency in your rack. + @timestamp = ("0", "0", "*", "*", "1"); + } elsif ($val =~ /quarterly/) { + @timestamp = ("0", "0", "1", "1,4,7,10", "*"); + } else { + # Advanced parsing for Day-Date-Time formats + my @parts = split / /, $val; + + # 1. Handle Day of Week (e.g., Mon..Fri) + if ($parts[0] =~ /[a-z]{3}/) { + my $days = shift @parts; + $days =~ s/monday/1/g; $days =~ s/mon/1/g; + $days =~ s/tuesday/2/g; $days =~ s/tue/2/g; + # ... (You'd want a map here) + $timestamp[4] = $days; + } - # Regex which matches the first three letters of - # the day of the week - my $dotw = "(?|(mon|fri|sun)(?:day)?|(tue)(?:sday)?|(wed)(?:nesday)?|(thu)(?:rsday)?|(sat)(?:urday)?)"; + # 2. Handle Date (YYYY-MM-DD) + if (@parts && $parts[0] =~ /\d+|\*/) { + my $date = shift @parts; + my ($y, $m, $d) = split /-/, $date; + $timestamp[3] = $m // "*"; + $timestamp[2] = $d // "*"; + } - my @ts_dotw = ("*"); - - if ($sysd_date[0] =~ /${dotw}(?:\.\.${dotw})?/i) { - @ts_dotw = (); - $days = shift @sysd_date; - foreach $day (split /,/, $days) { - $day =~ m/(${dotw})(?:\.\.(${dotw}))?/; - print $2, "\n"; - push @ts_dotw, (($1 == $2) ? "$1" : "$1-$2"); - } - } - - print @ts_dotw, "\n"; - $timestamp[4] = join ",", @ts_dotw; - - $date = shift @sysd_date; - print $date, "\n"; - $date =~ m/([0-9]{4}|\*)-([0-9]{1,2}|\*)-([0-9]{1,2}|\*)/; - if($1 != "*") { - print STDERR "Warning: Ignoring non-'*' year field in timer\n"; - } - $date =~ m/^([0-9]{4}|\*)-([0-9]{1,2}|\*)-([0-9]{1,2}|\*)$/; - $timestamp[2] = $2; - $timestamp[3] = $3; - - $time = shift @sysd_date; - print $time, "\n"; - $time =~ m/^([0-9]{1,2}):([0-9]{1,2})(?::([0-9]{1,2}))?$/; - if(int($3)) { - print STDERR "Warning: Ignoring non-zero seconds field in timer\n"; - } - $time =~ m/^([0-9]{1,2}):([0-9]{1,2})(?::([0-9]{1,2}))?$/; - $timestamp[0] = $1; - $timestamp[1] = $2; - print @timestamp, "\n"; - } - push @timestamps, \@timestamp; + # 3. Handle Time (HH:MM:SS) - THE CRITICAL FIX + if (@parts && $parts[0] =~ /(\d+):(\d+)/) { + $timestamp[0] = $2; # Minute + $timestamp[1] = $1; # Hour + } + } + push @timestamps, \@timestamp; } } -foreach $timestamp (@timestamps) { - print join " ", @$timestamp, "\n"; +foreach my $ts_ref (@timestamps) { + print join(" ", @$ts_ref), "\n"; } diff --git a/initify.pl b/initify.pl index 106fb7d..819cd3c 100755 --- a/initify.pl +++ b/initify.pl @@ -1,11 +1,11 @@ #!/usr/bin/perl -# (c) goose121, 2017 +# (c) goose121, 2017-2026 # Released under the MIT license +# Contributions for OpenRC Daemon/Placeholder support added 2026 use warnings; -#use strict; +use strict; use v5.10.1; -use feature "switch"; use Getopt::Long; use Pod::Usage; @@ -14,6 +14,8 @@ my @cmds_stop = (); my $pidfile = ""; my $desc = ""; +my $user = ""; +my $group = ""; # parse CLI args my %opt; @@ -34,91 +36,116 @@ } # get systemd service source file, and target service name -(my $service = $ARGV[0])=~s/\.service//; -(my $svc_name = $ARGV[0])=~s|(.*/)?([^/.]*)\.service|$2|; +(my $service = $ARGV[0]) =~ s/\.service//; +(my $svc_name = $ARGV[0]) =~ s|(.*/)?([^/.]*)\.service|$2|; $svc_name = $opt{name} if (length $opt{name}); -# begin +# parse systemd unit while(<>) { - #s/\s*|\s*$//g; # Trim whitespace - if (m/^Type\s*=\s*(.*)/) { - $type=$1; + chomp; + s/^\s+|\s+$//g; # Trim whitespace + + if (m/^Type\s*=\s*(.*)/i) { $type = lc($1); } + if (m/^Description\s*=\s*(.*)/i) { $desc = $1; } + if (m/^PIDFile\s*=\s*(.*)/i) { $pidfile = $1; } + if (m/^User\s*=\s*(.*)/i) { $user = $1; } + if (m/^Group\s*=\s*(.*)/i) { $group = $1; } + + if (m/^ExecStart\s*=\s*(.*)/i) { + if (length $1) { push(@cmds_start, $1); } + else { @cmds_start = (); } } - if (m/^ExecStart\s*=\s*(.*)/) { - if (length $1) { - push(@cmds_start, $1); - } - else { - @cmds_start = (); # If the line is "ExecStart=", - # it means to clear ExecStart. - } - } - if (m/^ExecStop\s*=\s*(.*)/) { - if (length $1) { - push(@cmds_stop, $1); - } - else { - @cmds_stop = (); # If the line is "ExecStop=", - # it means to clear ExecStop. - } - } - if (m/^PIDFile=(.*)/) { - if(length $1) { - $pidfile = $1; - } - } - if (m/^Description=(.*)/) { - $desc = $1 + if (m/^ExecStop\s*=\s*(.*)/i) { + if (length $1) { push(@cmds_stop, $1); } + else { @cmds_stop = (); } } } -my @cmds; - +# Prepare command parts my @cmd_path; my @cmd_argl; -map {my @sep = split(/ /, $_, 2); - push(@cmd_path, $sep[0]); - push(@cmd_argl, $sep[1]); -} @cmds_start; +foreach my $cmd (@cmds_start) { + my @sep = split(/ /, $cmd, 2); + push(@cmd_path, $sep[0]); + push(@cmd_argl, $sep[1] // ""); +} + +# --- OpenRC Logic Patches --- + +# 1. Handle Systemd placeholders (%i, %I) for OpenRC sub-services +# Maps %i to ${RC_SVCNAME#*.} so /etc/init.d/svc.instance works +foreach (@cmd_argl) { s/%[iI]/\${RC_SVCNAME#*.}/g; } +$pidfile =~ s/%[iI]/\${RC_SVCNAME#*.}/g; + +# 2. Determine Daemon/Backgrounding requirements +my $ssd_opts = ""; +if ($type eq "simple") { + # Simple types stay in foreground; OpenRC must daemonize them + $ssd_opts = "--background --make-pidfile"; + $pidfile = "/run/\${RC_SVCNAME}.pid" unless $pidfile; +} -open(FH, '>', "$service") || die("Cannot create $service: $!\n"); +# --- Generate OpenRC Script --- -print FH <<"EOF"; -\#!/sbin/openrc-run +open(my $fh, '>', "$service") || die("Cannot create $service: $!\n"); -command=$cmd_path[0] -command_args="$cmd_argl[0]" -pidfile=$pidfile +print $fh <<"EOF"; +#!/sbin/openrc-run +# Generated by initify - https://github.com/goose121/initify -name="$svc_name" +name="\$RC_SVCNAME" description="$desc" + +command="$cmd_path[0]" +command_args="$cmd_argl[0] $ssd_opts" +pidfile="$pidfile" EOF -close FH; +# Add User/Group if defined +print $fh "command_user=\"$user:$group\"\n" if ($user); + +print $fh <<'EOF'; +depend() { + after net + use dns logger +} + +start_pre() { + # Ensure runtime directory for PID exists + checkpath -d -m 0755 -o ${command_user:-root} /run/${RC_SVCNAME%/*} +} +EOF + +close $fh; +print "Successfully converted to OpenRC: $service\n"; __END__ =head1 NAME -initify - Convert systemd units to OpenRC init-files +initify - Convert systemd units to OpenRC init-files (Patched for 2026) =head1 SYNOPSIS - initify [options] file + initify [options] file.service + +=head2 Summary of Improvements + +=over 4 -=head2 Options +=item * Backgrounding -=over 12 +Automatically adds --background and --make-pidfile for Type=simple units. -=item -h, --help +=item * Placeholder Support -Print this message +Translates %i/%I to OpenRC-compatible shell variables for multi-instance scripts. -=item -n , --name= +=item * User/Group -Set the name of the unit created +Correctly maps Systemd User/Group to command_user. =back diff --git a/quadlet2openrc.pl b/quadlet2openrc.pl new file mode 100644 index 0000000..f2eb5d9 --- /dev/null +++ b/quadlet2openrc.pl @@ -0,0 +1,96 @@ +#!/usr/bin/perl +# (c) Necrohol & goose121, 2026 +# Transpiler: Podman Quadlet (.container) -> Gentoo OpenRC Runner +# Handles: CGroups v2, Resource Limits, Rootless vs Privileged, and Systemd Placeholders. + +use strict; +use warnings; +use FindBin qw($Bin); + +# --- Smart Require Logic --- +my $found = 0; +foreach my $path ("/usr/bin/initify", "$Bin/initify.pl", "./initify.pl") { + if (-e $path) { + require $path; + $found = 1; + last; + } +} +die "CRITICAL: initify logic not found! Check your ebuild or local files." unless $found; + +# Load cronify if available (optional) +foreach my $path ("/usr/bin/cronify", "$Bin/cronify.pl", "./cronify.pl") { + if (-e $path) { require $path; last; } +} + +# --- Initialization --- +my $input = $ARGV[0] or die "Usage: $0 \n"; +my @cgroup_rules; +my $is_rootless = 0; + +# --- Parse Input File for Resource Limits and Context --- +open(my $fh, '<', $input) or die "Could not open $input: $!"; +while (<$fh>) { + chomp; + s/\s*#.*$//; # Strip trailing comments + next unless /\S/; + + # 1. Detect User Context (Rootless Check) + if (/^(?:User|Group)\s*=\s*([^#\s]+)/i) { + my $val = lc($1); + $is_rootless = 1 if ($val ne 'root'); + } + + # 2. Map CPUQuota=150% -> cpu.max 150000 100000 + if (/^CPUQuota\s*=\s*(\d+)%/i) { + my $quota = $1 * 1000; + push @cgroup_rules, "cpu.max $quota 100000"; + } + + # 3. Map Memory Limits (Hard vs Soft) + if (/^MemoryHigh\s*=\s*(.+)/i) { + push @cgroup_rules, "memory.high $1"; + } + if (/^(?:MemoryMax|MemoryLimit)\s*=\s*(.+)/i) { + push @cgroup_rules, "memory.max $1"; + } +} +close($fh); + +# --- Capture Core Initify Output --- +my $output = ""; +{ + local *STDOUT; + open(STDOUT, '>', \$output); + initify::main($input); +} + +# --- Refine and Print Final OpenRC Script --- +my @lines = split("\n", $output); +foreach my $line (@lines) { + + # A. Inject CGroup v2 Settings before depend() + if ($line =~ /^depend\(\)/ && @cgroup_rules) { + print "# Resource Limits (Gentoo CGroup v2)\n"; + print "rc_cgroup_settings=\"\n"; + print " $_\n" for @cgroup_rules; + print "\"\n"; + print "rc_cgroup_cleanup=\"yes\"\n\n"; + undef @cgroup_rules; # Prevent double-injection + } + + # B. Smart Podman Injection (Privileged vs Rootless Namespace) + if ($input =~ /\.container$/ && $line =~ /^command=/) { + if ($line =~ /podman run/) { + if ($is_rootless) { + # Ensure user namespace persistence + $line =~ s/podman run/podman run --userns=keep-id/ if $line !~ /--userns/; + } else { + # Force privileged for hardware access (RPi5 GPIO, etc) + $line =~ s/podman run/podman run --privileged/ if $line !~ /--privileged/; + } + } + } + + print "$line\n"; +}