plover 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +201 -0
  4. data/Rakefile +14 -0
  5. data/lib/plover.rb +215 -0
  6. data/sig/plover.rbs +4 -0
  7. metadata +48 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c02ad2ecd60c71f6183a2db31ba64dd0f15769cbf7774273913595dfb72ccd93
4
+ data.tar.gz: c6fb12369964a4aa2238ffde51d4675e5b716eae056e12311193bb3570ec204e
5
+ SHA512:
6
+ metadata.gz: 15a087dd706ac377d6535956b9f62bb3a94e7811ce15c09e2a1b81ee3e79a2c25b26d495eedf5dd1a561e54b495d6c3867d9342a0d28ebbefc48fed7800c48b8
7
+ data.tar.gz: 7c8eee52f8818b246f1a57dd01947cb1ee398f12e92d0811559e0c3425ca93e5abfba0ecc2597c0dd091f74a48cf34feb7f159d43257f263c0b1eaab27c3d30b
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Charlton Trezevant
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # Plover
2
+
3
+ Plover is a tiny, embeddable Ruby build system designed to provide "Just Enough to Be Comfortable".
4
+
5
+ Plover is not a Make/Rake clone. It's intended to make it easier to write clean, organized build scripts in plain Ruby.
6
+
7
+ To help you with that, Plover provides a lightweight DSL for defining build phases, managing configuration flags, and tracking build artifacts, making it easier to manage configuration, state, and shared logic across your build and release processes.
8
+
9
+ ## Features
10
+
11
+ #### Tiny & Embeddable
12
+
13
+ Plover consists of ~200 lines of plain Ruby, with no dependencies outside of the standard library.
14
+
15
+ Because Plover is built to be embeddable, you can copy/paste `lib/plover.rb` into your project if you'd rather not use the gem.
16
+
17
+ #### Phased Build Process
18
+
19
+ Define your build steps in distinct phases (setup, before_build, build, after_build, teardown) using a clean DSL that doesn't get in your way.
20
+
21
+ #### Flags
22
+
23
+ Easily configure your builds using flags passed via environment variables or directly as parameters.
24
+
25
+ Plover will automatically pick up environment variables prefixed with `PLOVER_FLAG_` and merge them with your build configuration.
26
+
27
+ #### Artifacts
28
+
29
+ Track outputs from each build phase and retrieve them for further processing or validation.
30
+
31
+ #### Extensible
32
+
33
+ Plover provides a Concern system similar to ActiveSupport::Concern to help you organize and reuse common build functionality.
34
+
35
+
36
+ ## Installation
37
+
38
+ You can install Plover as a gem:
39
+
40
+ ```
41
+ gem install plover
42
+ ```
43
+
44
+ Or, add it to your project’s Gemfile:
45
+
46
+ ```
47
+ gem "plover"
48
+ ```
49
+
50
+ Because Plover is built to be embeddable, you can also just copy/paste `lib/plover.rb` into your project if you'd rather not use RubyGems (it's ~200 lines of plain Ruby, with no dependencies outside of the standard library).
51
+
52
+ ## Usage
53
+
54
+ Plover uses Plover to publish itself, so a real-world example can be found [in this repository](https://github.com/chtzvt/plover/blob/master/.github/workflows/publish.rb).
55
+
56
+
57
+ ### Basic Example
58
+
59
+ ```ruby
60
+ require "plover" # or require_relative "path/to/your/plover.rb"
61
+
62
+ class MyBuilder < Plover::Builder
63
+ # Require a flag named :my_flag
64
+ expect_flags(:my_flag)
65
+
66
+ # Setup phase: print a message
67
+ phase(:setup) do
68
+ puts "Setting up the build..."
69
+ end
70
+
71
+ # Build phase: simulate a build operation
72
+ phase(:build) do
73
+ puts "Building the project..."
74
+ do_a_thing if flag(:my_flag)
75
+ end
76
+
77
+ # Teardown phase: print a cleanup message
78
+ phase(:teardown) do
79
+ puts "Cleaning up..."
80
+ end
81
+
82
+ def do_a_thing
83
+ puts "I did a thing! #{flag(:my_flag)}"
84
+ end
85
+ end
86
+
87
+ # Create an instance with the required flag and run the build phases.
88
+ MyBuilder.new(my_flag: "cool").run
89
+ ```
90
+
91
+ ### Shared Concerns
92
+
93
+ ```ruby
94
+ require "plover"
95
+
96
+ # Define a concern under the Plover::Builder::Common namespace.
97
+ module Plover::Builder::Common::Greeting
98
+ extend Plover::Concern
99
+
100
+ # When this module is included, the `included` block is stored and later evaluated
101
+ # in the context of the including Builder.
102
+ included do
103
+ # Define an instance method.
104
+ def greet_builder
105
+ puts "(Concern) Hey there #{flag(:name)}!"
106
+ end
107
+
108
+ # Add a phase step in the setup phase.
109
+ phase(:setup) do
110
+ puts "A Builder and a Concern enter:"
111
+ end
112
+
113
+ # Prepend a step to the build phase that calls the class method from the concern.
114
+ prepend_phase(:build) do
115
+ self.class.greet_class
116
+ end
117
+
118
+ # Prepend a step to the after_build phase.
119
+ prepend_phase(:after_build) do
120
+ greet_builder
121
+ puts "(Concern) See you later."
122
+ end
123
+
124
+ # Add a step to the teardown phase.
125
+ phase(:teardown) do
126
+ puts "fin."
127
+ end
128
+ end
129
+
130
+ # Define class methods that will be extended onto the including class.
131
+ class_methods do
132
+ def greet_class
133
+ puts "(Concern) Hello! I'm a Concern. We can prepend or append steps to phases, define class or instance methods, and access state."
134
+ end
135
+ end
136
+ end
137
+
138
+ # Define a builder subclass that uses the Greeting concern.
139
+ class MyBuilder < Plover::Builder
140
+ # Include the concern by specifying its name.
141
+ common_include "Greeting"
142
+
143
+ # Optionally, you could include multiple concerns:
144
+ # common_include "Greeting", "SomethingElse"
145
+ # or include all common modules:
146
+ # common_include_all
147
+
148
+ # Register a build phase step that calls a builder method.
149
+ phase(:build) do
150
+ builder_greeting
151
+ end
152
+
153
+ # Register an after_build phase step.
154
+ phase(:after_build) do
155
+ builder_goodbye
156
+ end
157
+
158
+ def builder_greeting
159
+ # Set a flag that can be used by the concern.
160
+ set_flag(:name, self.class.name)
161
+ puts "(Builder) Hi! I'm a Builder named #{flag(:name)}."
162
+ end
163
+
164
+ def builder_goodbye
165
+ puts "(Builder) Goodbye!"
166
+ end
167
+ end
168
+
169
+ # Instantiate and run the builder.
170
+ MyBuilder.new.run
171
+ ```
172
+
173
+
174
+ ## Running Your Build
175
+
176
+ Plover is typically invoked by running your build script (e.g. ruby path/to/your/Ploverfile.rb). You can pass configuration flags via environment variables:
177
+
178
+ ```
179
+ PLOVER_FLAG_GEM_VERSION="v0.1.0" \
180
+ PLOVER_FLAG_RUBYGEMS_RELEASE_TOKEN="YOUR_TOKEN" \
181
+ PLOVER_FLAG_GITHUB_RELEASE_TOKEN="YOUR_TOKEN" \
182
+ ruby path/to/your/Ploverfile.rb
183
+ ```
184
+
185
+ Environment variables will automatically be configured as Builder flags with a common convention (`PLOVER_FLAG_MY_OPTION` becomes `flag(:my_option)`).
186
+
187
+ ## Contributing
188
+
189
+ Contributions, issues, and feature requests are welcome!
190
+ Feel free to check issues page.
191
+
192
+ 1. Fork the repository.
193
+ 2. Create a new branch (`git switch -c feature/my-feature`).
194
+ 3. Make your changes.
195
+ 4. Submit a pull request.
196
+
197
+
198
+ ## License
199
+
200
+ This project is licensed under the MIT License. See LICENSE for details.
201
+
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "rspec/core/rake_task"
6
+
7
+ require "standard/rake"
8
+
9
+ RSpec::Core::RakeTask.new(:spec) do |t|
10
+ t.ruby_opts = %w[-w]
11
+ end
12
+
13
+ task test: %i[spec standard]
14
+ task default: %i[test]
data/lib/plover.rb ADDED
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plover
4
+ require "shellwords"
5
+ require "fileutils"
6
+
7
+ VERSION = "1.0.0"
8
+
9
+ class PloverError < StandardError; end
10
+
11
+ module Concern
12
+ def self.extended(base)
13
+ base.instance_variable_set(:@_included_blocks, [])
14
+ end
15
+
16
+ def included(base = nil, &block)
17
+ return unless base.nil?
18
+
19
+ @_included_blocks << block if block_given?
20
+ end
21
+
22
+ def class_methods(&block)
23
+ const_set(:ClassMethods, Module.new(&block))
24
+ end
25
+
26
+ def apply_concern(base)
27
+ base.extend(const_get(:ClassMethods)) if const_defined?(:ClassMethods)
28
+ @_included_blocks.each { |blk| base.class_eval(&blk) }
29
+ base.include(self)
30
+ end
31
+ end
32
+
33
+ class Builder
34
+ class FlagError < PloverError; end
35
+
36
+ class BuildError < PloverError; end
37
+
38
+ class ArtifactError < BuildError; end
39
+
40
+ module Common; end
41
+
42
+ attr_reader :configuration
43
+
44
+ @configuration = {
45
+ steps: {
46
+ setup: [],
47
+ before_build: [],
48
+ build: [],
49
+ after_build: [],
50
+ teardown: []
51
+ },
52
+ options: {
53
+ flags: {},
54
+ expected_flags: []
55
+ },
56
+ include_common: :none,
57
+ artifacts: {
58
+ setup: {},
59
+ before_build: {},
60
+ build: {},
61
+ after_build: {},
62
+ teardown: {}
63
+ }
64
+ }
65
+
66
+ class << self
67
+ attr_reader :configuration
68
+
69
+ def inherited(subclass)
70
+ auto_include_common
71
+ subclass.instance_variable_set(:@configuration, deep_copy(configuration))
72
+ end
73
+
74
+ def set_flag(name, value)
75
+ configuration[:options][:flags][name.to_sym] = value
76
+ end
77
+
78
+ def expect_flags(*flags)
79
+ configuration[:options][:expected_flags] = flags.map(&:to_sym)
80
+ end
81
+
82
+ def common_include_all
83
+ configuration[:options][:common_include] = :all
84
+ end
85
+
86
+ def common_include_none
87
+ configuration[:options][:common_include] = :none
88
+ end
89
+
90
+ def common_include(*modules)
91
+ configuration[:options][:common_include] = [] unless configuration[:options][:common_include].is_a?(Array)
92
+
93
+ configuration[:options][:common_include].concat(modules)
94
+ end
95
+
96
+ def env_flags
97
+ ENV.select { |key, _| key.start_with?("PLOVER_FLAG_") }
98
+ .map { |key, value| [key.sub(/^PLOVER_FLAG_/, "").downcase.to_sym, value] }
99
+ .to_h
100
+ end
101
+
102
+ def phase(phase, &block)
103
+ configuration[:steps][phase] << block
104
+ end
105
+
106
+ def prepend_phase(phase, &block)
107
+ configuration[:steps][phase].unshift(block)
108
+ end
109
+
110
+ def auto_include_common
111
+ return if configuration[:options][:common_include] == :none
112
+
113
+ ObjectSpace.each_object(Module).select { |m| m&.name&.start_with?("Plover::Builder::Common::") }.each do |mod|
114
+ next if mod.name.to_s.end_with?("::ClassMethods")
115
+
116
+ next if configuration[:options][:common_include].is_a?(Array) && !configuration[:options][:common_include].any? { |m| m.end_with?("::#{mod.name}") }
117
+
118
+ mod.apply_concern(self) if mod.respond_to?(:apply_concern) && !included_modules.include?(mod)
119
+ end
120
+ end
121
+
122
+ def deep_copy(obj)
123
+ case obj
124
+ when Hash
125
+ obj.each_with_object({}) { |(k, v), h| h[k] = deep_copy(v) }
126
+ when Array
127
+ obj.map { |e| deep_copy(e) }
128
+ else
129
+ obj
130
+ end
131
+ end
132
+ end
133
+
134
+ def initialize(flags = {})
135
+ self.class.auto_include_common
136
+ @configuration = self.class.configuration
137
+ @configuration[:options][:flags] = @configuration[:options][:flags].merge(flags).merge(self.class.env_flags)
138
+
139
+ return unless @configuration[:options][:expected_flags].any?
140
+
141
+ missing_flags = @configuration[:options][:expected_flags].reject { |sym| @configuration[:options][:flags].key?(sym) }
142
+ raise BuildError.new("Missing required flags: #{missing_flags.join(", ")}") if missing_flags.any?
143
+ end
144
+
145
+ def prepend_phase(phase, &block)
146
+ @configuration[:steps][phase].unshift(block)
147
+ end
148
+
149
+ def append_phase(phase, &block)
150
+ @configuration[:steps][phase] << block
151
+ end
152
+
153
+ def esc(str)
154
+ Shellwords.escape(str)
155
+ end
156
+
157
+ def flag(name)
158
+ @configuration[:options][:flags][name.to_sym]
159
+ end
160
+
161
+ def esc_flag(name)
162
+ flag(name) ? esc(flag(name)) : nil
163
+ end
164
+
165
+ def set_flag(name, value)
166
+ @configuration[:options][:flags][name.to_sym] = value
167
+ end
168
+
169
+ def raise_unless_flag(name, message)
170
+ raise FlagError.new(message) unless flag(name)
171
+ end
172
+
173
+ def push_artifact(name, value)
174
+ return unless @current_phase
175
+ @configuration[:artifacts][@current_phase][name] = value
176
+ end
177
+
178
+ def esc_artifact(phase, name)
179
+ artifact = self.artifact(phase, name)
180
+ artifact ? esc(artifact) : nil
181
+ end
182
+
183
+ def artifact(phase, name)
184
+ @configuration[:artifacts][phase][name]
185
+ end
186
+
187
+ def artifacts(phase = nil)
188
+ phase ? @configuration[:artifacts][phase] : @configuration[:artifacts]
189
+ end
190
+
191
+ def raise_unless_artifact(phase, name, message)
192
+ raise ArtifactError.new(message) unless artifact(phase, name)
193
+ end
194
+
195
+ def fail_build(message)
196
+ raise BuildError.new(message)
197
+ end
198
+
199
+ def run_phase(phase)
200
+ @current_phase = phase
201
+ @configuration[:steps][phase].each { |block| instance_exec(&block) }
202
+ @current_phase = nil
203
+ end
204
+
205
+ def run
206
+ run_phase(:setup)
207
+ Dir.chdir(flag(:build_root) || ".") do
208
+ run_phase(:before_build)
209
+ run_phase(:build)
210
+ run_phase(:after_build)
211
+ end
212
+ run_phase(:teardown)
213
+ end
214
+ end
215
+ end
data/sig/plover.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Plover
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plover
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Charlton Trezevant
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-03-17 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Plover is a tiny, embeddable Ruby build system.
13
+ email:
14
+ - ct@ctis.me
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE.txt
20
+ - README.md
21
+ - Rakefile
22
+ - lib/plover.rb
23
+ - sig/plover.rbs
24
+ homepage: https://github.com/chtzvt/plover
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ homepage_uri: https://github.com/chtzvt/plover
29
+ source_code_uri: https://github.com/chtzvt/plover
30
+ changelog_uri: https://github.com/chtzvt/plover/releases
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 3.1.0
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.6.2
46
+ specification_version: 4
47
+ summary: Plover is a tiny, embeddable Ruby build system.
48
+ test_files: []