Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Human-facing entry points for resolved provisioning plans. The generated
# files remain disposable compiler output under scriptbox/generated/.

[unix]
set shell := ["bash", "-uc"]

[windows]
set shell := ["powershell.exe", "-NoLogo", "-Command"]

generate-ubuntu *args:
ruby scriptbox/scripts/generate_justfile.rb scriptbox/config/ubuntu2204.yml --output scriptbox/generated/ubuntu2204.just {{args}}

install-ubuntu:
just --justfile scriptbox/generated/ubuntu2204.just install

generate-macos *args:
ruby scriptbox/scripts/generate_justfile.rb scriptbox/config/macos.yml --output scriptbox/generated/macos.just {{args}}

install-macos:
just --justfile scriptbox/generated/macos.just install

generate-windows *args:
ruby scriptbox/scripts/generate_justfile.rb scriptbox/config/windows.yml --output scriptbox/generated/windows.just {{args}}

install-windows:
just --justfile scriptbox/generated/windows.just install

generate-msys2 *args:
ruby scriptbox/scripts/generate_justfile.rb scriptbox/config/msys2.yml --output scriptbox/generated/msys2.just {{args}}

install-msys2:
just --justfile scriptbox/generated/msys2.just install

generate-cygwin *args:
ruby scriptbox/scripts/generate_justfile.rb scriptbox/config/cygwin.yml --output scriptbox/generated/cygwin.just {{args}}

install-cygwin:
just --justfile scriptbox/generated/cygwin.just install
7 changes: 7 additions & 0 deletions scriptbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ This area has information about installation and useful scripts.
* Ruby for any `.rb` scripts
* Powershell for any `.ps1` scripts

## Justfile Support

You will need to install just to be able to use Justfiles.

* https://github.com/casey/just
* https://just.systems/man/en/

## RatatuiRuby Library

Some scripts will use the [RatatuiRuby](https://www.ratatui-ruby.dev/) TUI library.
Expand Down
7 changes: 4 additions & 3 deletions scriptbox/generated/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# generated/

Output of `scripts/gen_installer.rb` (and `scripts/generate_install_sh.rb` in
single-file mode) lands here - one `<platform>_install.sh` per platform, built
from `config/*.yml`.
Output of `scripts/gen_installer.rb`, `scripts/generate_install_script.rb`, and
`scripts/generate_justfile.rb` lands here. Outputs include standalone install
scripts and resolved `<platform>.just` execution plans built from
`config/*.yml`.

Everything in this folder except this README is gitignored and gets
regenerated on demand:
Expand Down
20 changes: 20 additions & 0 deletions scriptbox/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ every change - a deliberately-triggered suite, one letter at a time.

This script generates an install script: GNU Bash (`.sh`) for POSIX environments and powershell (`.ps1`) for Windows. The scripts will vary depending on the options selected.

## generate_justfile.rb

Generates a resolved Justfile from the same manifest, selection, dependency,
and command-rendering pipeline as the standalone installer. The generated
`install` recipe is deliberately one script recipe so exports, sourced version
manager initialization, shell functions, and working-directory changes remain
in one interpreter process.

```bash
./generate_justfile.rb ../config/ubuntu2204.yml \
--select rbenv,pyenv,sdkman_groovy \
--output ../generated/ubuntu2204.just

just --justfile ../generated/ubuntu2204.just install
```

Generation checks `config/env.yml` and rejects a manifest that does not match
the current environment. Native Windows, MSYS2, and Cygwin are separate
platforms. Use `--allow-host-mismatch` for deliberate cross-generation.

### Ubuntu 22.04

These are some combinations you would try below:
Expand Down
131 changes: 131 additions & 0 deletions scriptbox/scripts/generate_justfile.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env ruby
require 'fileutils'
require 'optparse'
require 'tempfile'
require 'yaml'
require_relative 'generate_install_script'
require_relative 'verify_commands'

# A Justfile is an execution backend, not a second resolver. Generate the
# ordinary, fully-resolved installer first, then place that exact program in a
# single script recipe. Keeping the plan in one interpreter process is
# important: invoking `just` from a terminal does not make each recipe shell
# interactive, and separate recipes would lose exports/functions established
# by earlier installation steps.

def just_platform_supported?(config_path, platform, detected)
env_path = File.join(File.dirname(config_path), 'env.yml')
return nil unless File.file?(env_path)

environments = YAML.load_file(env_path)['environments'] || []
entry = environments.find { |candidate| candidate['platform'].to_s.casecmp?(platform) }
entry && Array(entry['supports']).include?(detected)
end

def indent_just_recipe(body)
body.lines.map { |line| line.strip.empty? ? "\n" : " #{line}" }.join
end

def write_resolved_justfile(config_path, out_path, select_tags: [], exclude_tags: [],
selectors: [], check_host: true)
tree = YAML.load_file(config_path)
platform = root_key(tree)
raise "no platform manifest found in #{config_path}" unless platform

detected = uname_string
supported = just_platform_supported?(config_path, platform, detected)
if check_host && supported == false
raise "detected environment '#{detected}' is not supported for platform '#{platform}' " \
"(use --allow-host-mismatch only when intentionally cross-generating)"
end

tree = substitute_variables(tree, tree[platform]['variables'] || {})
natural_steps = flatten(tree[platform])
steps, omitted = resolve_included(
natural_steps.dup, select_tags, exclude_tags,
selectors: expand_selectors(selectors)
)
topological_order(steps)
dedup!(steps)

header_lines = [
"Generated by generate_justfile.rb from #{File.basename(config_path)} - do not edit by hand.",
"Resolved platform: #{platform}",
"Detected generation environment: #{detected}"
]
omitted.each do |step, missing|
header_lines << "Omitted: [#{step[:path]}] #{step[:type]}: #{step[:name]} - " \
"needs '#{missing}', no eligible provider"
end
check_version_needs!(steps).each { |step| header_lines << "Omitted: #{step[:omitted_reason]}" }

dialect = dialect_for(tree, platform)
Tempfile.create(["#{platform}_resolved", dialect == 'powershell' ? '.ps1' : '.sh']) do |installer|
installer.close
write_install_script(
platform, steps, tree, dialect, File.dirname(installer.path), header_lines,
natural_steps, apt_mirror_for(tree, platform), out_path: installer.path
)
program = File.binread(installer.path).sub(/\A#![^\n]*\n/, '')
if dialect == 'powershell'
# just writes a script recipe to a temporary file. Keep the parent
# invocation alive until the elevated child finishes, or just may remove
# that temporary file before the child has opened $PSCommandPath.
program = program.sub('Start-Process powershell.exe -Verb RunAs -ArgumentList ',
'Start-Process powershell.exe -Verb RunAs -Wait -ArgumentList ')
end

FileUtils.mkdir_p(File.dirname(File.expand_path(out_path)))
File.open(out_path, 'wb') do |file|
file.puts '# Generated file. Change the platform manifest or generator instead.'
file.puts "# Source: #{File.basename(config_path)}"
file.puts "# Platform: #{platform}; generated on: #{detected}"
file.puts
if dialect == 'powershell'
file.puts '[script("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File")]'
else
file.puts '[script("bash")]'
end
file.puts '# Install the fully resolved provisioning plan.'
file.puts 'install:'
file.write(indent_just_recipe(program))
end
end

out_path
end

if __FILE__ == $PROGRAM_NAME
options = { select: [], exclude: [], selectors: [], check_host: true }
parser = OptionParser.new do |opts|
opts.banner = 'usage: generate_justfile.rb <config.yml> [SECTION ...] --output PATH [options]'
opts.on('--select TAGS', 'comma-separated provider tags to enable') do |value|
options[:select] = value.split(',').map(&:strip)
end
opts.on('--exclude TAGS', 'comma-separated tags to veto') do |value|
options[:exclude] = value.split(',').map(&:strip)
end
opts.on('--output PATH', 'resolved Justfile path (required)') { |value| options[:output] = value }
opts.on('--allow-host-mismatch', 'permit intentional cross-generation') { options[:check_host] = false }
end
parser.parse!

config_path = ARGV.shift
options[:selectors] = ARGV
if config_path.nil? || options[:output].nil?
warn parser
exit 1
end

begin
path = write_resolved_justfile(
config_path, options[:output], select_tags: options[:select],
exclude_tags: options[:exclude], selectors: options[:selectors],
check_host: options[:check_host]
)
puts "wrote #{path}"
rescue StandardError => error
warn "generate_justfile.rb: #{error.message}"
exit 1
end
end
58 changes: 58 additions & 0 deletions scriptbox/scripts/test/test_generate_justfile.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
require_relative 'test_helper'
require_relative '../generate_justfile'
require 'tmpdir'

class TestGenerateJustfile < Minitest::Test
CONFIG_PATH = File.expand_path('../../config/ubuntu2204.yml', __dir__)

def generate(select = [])
Dir.mktmpdir do |dir|
path = File.join(dir, 'ubuntu2204.just')
write_resolved_justfile(
CONFIG_PATH, path, select_tags: select, check_host: false,
selectors: ['lessons.gen_scripts.{ruby,python3}']
)
return File.binread(path)
end
end

def test_emits_one_bash_script_recipe
output = generate

assert_includes output, '[script("bash")]'
assert_includes output, "install:\n"
assert_includes output, ' set -e'
refute_includes output, '#!/bin/bash'
end

def test_uses_the_existing_resolved_provider_selection
output = generate(%w[asdf asdf_ruby asdf_python])

assert_includes output, 'asdf install ruby 4.0.6'
assert_includes output, 'asdf install python 3.14.7'
refute_includes output, 'rbenv install'
refute_includes output, 'pyenv install'
end

def test_platform_matching_distinguishes_windows_shell_environments
env_path = File.expand_path('../../config/env.yml', __dir__)
dir = File.dirname(env_path)

assert_equal true, just_platform_supported?(File.join(dir, 'windows.yml'), 'windows', 'windows_nt.10_0_26200')
assert_equal false, just_platform_supported?(File.join(dir, 'windows.yml'), 'windows', 'mingw64_nt.10_0_26200')
assert_equal true, just_platform_supported?(File.join(dir, 'msys2.yml'), 'msys2', 'mingw64_nt.10_0_26200')
assert_equal true, just_platform_supported?(File.join(dir, 'cygwin.yml'), 'cygwin', 'cygwin_nt.10_0_26200')
end

def test_windows_elevation_waits_while_justs_temporary_script_exists
config = File.expand_path('../../config/windows.yml', __dir__)
Dir.mktmpdir do |dir|
path = File.join(dir, 'windows.just')
write_resolved_justfile(config, path, check_host: false)
output = File.binread(path)

assert_includes output, '[script("powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File")]'
assert_includes output, 'Start-Process powershell.exe -Verb RunAs -Wait -ArgumentList'
end
end
end
Loading