# frozen_string_literal: true module FeatherCaseWizard class ToolError < StandardError; end class Toolchain attr_reader :openscad, :blender, :python def initialize(env: ENV) @env = env @openscad = discover("OPENSCAD", "openscad") @blender = discover("BLENDER", "blender") @python = discover("PYTHON", "python3") end def check! missing = [] missing << "OpenSCAD (`sudo apt-get install openscad` or set OPENSCAD)" unless openscad missing << "Blender (`sudo apt-get install blender` or set BLENDER)" unless blender missing << "Python 3 (`sudo apt-get install python3` or set PYTHON)" unless python return self if missing.empty? raise ToolError, "Missing required tools:\n - #{missing.join("\n - ")}" end def run!(*command, chdir: nil, env: {}) options = chdir ? {chdir: chdir} : {} stdout, stderr, status = Open3.capture3(env, *command, **options) return [stdout, stderr] if status.success? detail = [stdout, stderr].reject(&:empty?).join("\n").strip result = status.exitstatus || "signal #{status.termsig}" raise ToolError, "Command failed (#{result}): #{command.first}\n#{detail}" rescue Errno::ENOENT => e raise ToolError, "Cannot run #{command.first}: #{e.message}" end private def discover(env_name, command) override = @env[env_name] return File.expand_path(override) if override && File.executable?(override) @env.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |directory| candidate = File.join(directory, command) return candidate if File.file?(candidate) && File.executable?(candidate) end nil end end end