Skip to content

Scheduler fails on NixOS: invalid OnCalendar format, hardcoded /usr/bin/perl, macOS-only PATH #14

Description

@KaminariOS

Environment

  • OS: NixOS (Linux, systemd-based)
  • opencode: 1.2.26
  • opencode-scheduler: latest

Problem

The scheduler plugin generates systemd timer/service units that fail immediately on NixOS with:

Failed to start opencode-job-....timer: Unit ... has a bad unit file setting.

There are three distinct bugs, all in src/index.ts:


Bug 1: Invalid OnCalendar format (line 841)

Function: cronToSystemdCalendars

The generated calendar spec includes a leading * as a day-of-week field:

OnCalendar=* *-*-* 08:00:00

systemd does not accept * as a day-of-week prefix. The correct format when no specific day is constrained is simply:

OnCalendar=*-*-* 08:00:00

systemd logs confirm:

Failed to parse calendar specification, ignoring: * *-*-* 08:00:00
Timer unit lacks value setting. Refusing.

Root cause — line 841:

calendars.push(`${dowValue} *-${monthValue}-${domValue} ${hourValue}:${minuteValue}:00`)

When dowValue is "*", it produces an invalid spec. The day-of-week prefix should only be included when it's an actual day name (Mon, Tue, etc.).

Proposed fix:

const prefix = dowValue === "*" ? "" : `${dowValue} `;
calendars.push(`${prefix}*-${monthValue}-${domValue} ${hourValue}:${minuteValue}:00`);

Bug 2: Hardcoded /usr/bin/perl (lines 1052, 1160, 1412)

The ExecStart in generated systemd service units hardcodes /usr/bin/perl:

ExecStart="/usr/bin/perl" "/home/.../.config/opencode/scheduler/supervisor.pl" "..."

On NixOS, perl lives in the Nix store (e.g., /nix/store/...-perl-5.x.y/bin/perl) or is available via /run/current-system/sw/bin/perl. /usr/bin/perl does not exist.

This affects:

  • createSystemdService (line 1160) — Linux
  • createLaunchdPlist (line 1052) — macOS
  • createCronEntry (line 1412) — fallback

Proposed fix: Detect the perl path dynamically at job creation time:

import { execSync } from "child_process";

function findPerl(): string {
  try {
    return execSync("command -v perl", { encoding: "utf-8" }).trim();
  } catch {
    return "/usr/bin/perl"; // fallback
  }
}

Bug 3: macOS-only PATH in generated units (lines 663-673)

Function: getEnhancedPath

function getEnhancedPath(): string {
  const paths = [
    "/opt/homebrew/bin",
    "/usr/local/bin",
    "/usr/bin",
    "/bin",
    "/usr/sbin",
    "/sbin",
  ]
  return paths.join(":")
}

This is used as Environment="PATH=..." in the systemd service unit (line 1170). On NixOS, none of these contain any user tools — binaries are in /nix/store/..., ~/.nix-profile/bin, /run/current-system/sw/bin, etc.

Even if bug 2 were fixed to use a bare perl command, it would still fail because PATH doesn't include any NixOS paths.

Proposed fix: Capture and embed the current process.env.PATH (which contains the correct paths at plugin load time), with the hardcoded paths as fallbacks:

function getEnhancedPath(): string {
  const fallbackPaths = [
    "/opt/homebrew/bin",
    "/usr/local/bin",
    "/usr/bin",
    "/bin",
    "/usr/sbin",
    "/sbin",
  ];

  const currentPath = process.env.PATH || "";
  if (currentPath) {
    // Deduplicate: append fallbacks that aren't already present
    const existing = new Set(currentPath.split(":"));
    const extras = fallbackPaths.filter((p) => !existing.has(p));
    return [currentPath, ...extras].join(":");
  }

  return fallbackPaths.join(":");
}

Workaround

For anyone hitting this on NixOS, you can manually fix the generated units after job creation:

  1. Fix the timer — remove the leading * from OnCalendar:

    OnCalendar=*-*-* 08:00:00
  2. Fix the service — update ExecStart to use the NixOS perl path:

    ExecStart=/run/current-system/sw/bin/perl ...
  3. Fix PATH:

    Environment="PATH=/run/current-system/sw/bin:/home/<user>/.nix-profile/bin:/nix/var/nix/profiles/default/bin:/usr/bin:/bin"
  4. Reload and enable:

    systemctl --user daemon-reload
    systemctl --user enable --now <timer-unit>

Reproduction

# On NixOS, any schedule_job call will fail:
# opencode scheduler creates the units, then runs:
#   systemctl --user start <timer>
# which immediately fails with "bad unit file setting"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions