rundoc 2.0.1 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +10 -7
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +25 -0
- data/README.md +35 -48
- data/Rakefile +5 -1
- data/bin/rundoc +4 -17
- data/lib/rundoc/cli.rb +208 -49
- data/lib/rundoc/cli_argument_parser.rb +140 -0
- data/lib/rundoc/code_command/bash.rb +8 -1
- data/lib/rundoc/code_command/rundoc/depend_on.rb +1 -24
- data/lib/rundoc/code_command/rundoc/require.rb +13 -7
- data/lib/rundoc/code_command/website/driver.rb +7 -5
- data/lib/rundoc/code_command/website/navigate.rb +1 -1
- data/lib/rundoc/code_command/website/screenshot.rb +7 -2
- data/lib/rundoc/code_command/website/visit.rb +4 -2
- data/lib/rundoc/code_section.rb +7 -7
- data/lib/rundoc/context/after_build.rb +14 -0
- data/lib/rundoc/context/execution.rb +22 -0
- data/lib/rundoc/parser.rb +8 -4
- data/lib/rundoc/version.rb +1 -1
- data/lib/rundoc.rb +13 -5
- data/rundoc.gemspec +1 -0
- data/test/fixtures/cnb/ruby/download.md +22 -0
- data/test/fixtures/cnb/ruby/image_structure.md +34 -0
- data/test/fixtures/cnb/ruby/intro.md +5 -0
- data/test/fixtures/cnb/ruby/multiple_langs.md +43 -0
- data/test/fixtures/cnb/ruby/rundoc.md +48 -0
- data/test/fixtures/cnb/ruby/what_is_pack_build.md +18 -0
- data/test/fixtures/cnb/shared/call_to_action.md +11 -0
- data/test/fixtures/cnb/shared/configure_builder.md +23 -0
- data/test/fixtures/cnb/shared/install_pack.md +14 -0
- data/test/fixtures/cnb/shared/pack_build.md +20 -0
- data/test/fixtures/cnb/shared/procfile.md +13 -0
- data/test/fixtures/cnb/shared/use_the_image.md +52 -0
- data/test/fixtures/cnb/shared/what_is_a_builder.md +18 -0
- data/test/fixtures/rails_4/rundoc.md +1 -1
- data/test/fixtures/rails_5/rundoc.md +1 -1
- data/test/fixtures/rails_7/rundoc.md +0 -1
- data/test/fixtures/rails_8/rundoc.md +481 -0
- data/test/fixtures/simple_git/rundoc.md +13 -0
- data/test/integration/after_build_test.rb +62 -0
- data/test/integration/print_test.rb +9 -9
- data/test/integration/require_test.rb +63 -0
- data/test/integration/website_test.rb +35 -0
- data/test/rundoc/cli_argument_parser_test.rb +118 -0
- data/test/rundoc/code_section_test.rb +40 -8
- data/test/rundoc/parser_test.rb +3 -3
- data/test/rundoc/peg_parser_test.rb +6 -6
- data/test/system/exe_cli_test.rb +231 -0
- data/test/test_helper.rb +74 -1
- metadata +41 -3
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "optparse"
|
5
|
+
|
6
|
+
module Rundoc
|
7
|
+
# This class is responsible for parsing the command line arguments and generating a Cli instance
|
8
|
+
#
|
9
|
+
# Example:
|
10
|
+
#
|
11
|
+
# cli = CLIArgumentParser.new(argv: ARGV).to_cli
|
12
|
+
# cli.call
|
13
|
+
#
|
14
|
+
class CLIArgumentParser
|
15
|
+
attr_reader :io, :env, :exit_obj, :options, :argv
|
16
|
+
|
17
|
+
def initialize(
|
18
|
+
argv:,
|
19
|
+
io: $stderr,
|
20
|
+
env: ENV,
|
21
|
+
exit_obj: Kernel
|
22
|
+
)
|
23
|
+
@io = io
|
24
|
+
@env = env
|
25
|
+
@argv = argv
|
26
|
+
@options = {}
|
27
|
+
@exit_obj = exit_obj
|
28
|
+
end
|
29
|
+
|
30
|
+
def call
|
31
|
+
source_file = argv.first
|
32
|
+
if source_file.nil? || source_file == "help"
|
33
|
+
parser.parse! ["--help"]
|
34
|
+
return
|
35
|
+
else
|
36
|
+
parser.parse! argv
|
37
|
+
return if options[:exit]
|
38
|
+
end
|
39
|
+
|
40
|
+
source_path = Pathname(source_file)
|
41
|
+
if !source_path.exist?
|
42
|
+
@io.puts "No such file `#{source_path.expand_path}`"
|
43
|
+
exit_obj.exit(1)
|
44
|
+
return
|
45
|
+
elsif !source_path.file?
|
46
|
+
@io.puts "Path is not a file. Expected `#{source_path.expand_path}` to be a file, but it was not."
|
47
|
+
exit_obj.exit(1)
|
48
|
+
return
|
49
|
+
end
|
50
|
+
|
51
|
+
options[:io] = io
|
52
|
+
options[:source_path] = source_path
|
53
|
+
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
private def parser
|
58
|
+
@parser ||= OptionParser.new do |opts|
|
59
|
+
opts.banner = <<~EOF
|
60
|
+
Usage: $ rundoc [options] <path/to/RUNDOC.md>
|
61
|
+
|
62
|
+
Reads a custom markdown file and executes the code blocks within it to produce a tutorial with real world outputs embedded.
|
63
|
+
|
64
|
+
Produces:
|
65
|
+
|
66
|
+
- A directory with any generated files
|
67
|
+
- A #{CLI::DEFAULTS::OUTPUT_FILENAME} file with the generated output
|
68
|
+
- A screenshots directory with any screenshots taken
|
69
|
+
|
70
|
+
## Example
|
71
|
+
|
72
|
+
A rundoc file:
|
73
|
+
|
74
|
+
~$ cat path/to/RUNDOC.md
|
75
|
+
```
|
76
|
+
:::>> $ echo "hello world"
|
77
|
+
:::>> $ touch grass.txt
|
78
|
+
```
|
79
|
+
|
80
|
+
Is executed with the rundoc command
|
81
|
+
|
82
|
+
~$ rundoc path/to/RUNDOC.md
|
83
|
+
# ...
|
84
|
+
|
85
|
+
Produces files on disk:
|
86
|
+
|
87
|
+
~$ ls path/to/#{CLI::DEFAULTS::ON_SUCCESS_DIR}
|
88
|
+
#{CLI::DEFAULTS::OUTPUT_FILENAME}
|
89
|
+
grass.txt
|
90
|
+
|
91
|
+
And replaces the rundoc syntax with the result of the real output:
|
92
|
+
|
93
|
+
~$ cat path/to/#{CLI::DEFAULTS::ON_SUCCESS_DIR}/#{CLI::DEFAULTS::OUTPUT_FILENAME}
|
94
|
+
```
|
95
|
+
$ echo "hello world"
|
96
|
+
hello world
|
97
|
+
$ touch grass.txt
|
98
|
+
```
|
99
|
+
|
100
|
+
## Options
|
101
|
+
|
102
|
+
> Note: Current working directory is abbreviated CWD
|
103
|
+
|
104
|
+
EOF
|
105
|
+
|
106
|
+
opts.on("--help", "Prints this help output.") do |_|
|
107
|
+
@io.puts opts
|
108
|
+
options[:exit] = true
|
109
|
+
@exit_obj.exit(0)
|
110
|
+
end
|
111
|
+
|
112
|
+
opts.on("--on-success-dir <dir>", "Success dir, relative to CWD. i.e. `<rundoc.md/dir>/#{CLI::DEFAULTS::ON_SUCCESS_DIR}/`.") do |v|
|
113
|
+
options[:on_success_dir] = v
|
114
|
+
end
|
115
|
+
|
116
|
+
opts.on("--on-failure-dir <dir>", "Failure dir, relative to CWD i.e. `<rundoc.md/dir>/#{CLI::DEFAULTS::ON_FAILURE_DIR}/`.") do |v|
|
117
|
+
options[:on_failure_dir] = v
|
118
|
+
end
|
119
|
+
|
120
|
+
opts.on("--dotenv-path <path>", "Environment variable file, relative to CWD. i.e. `<rundoc.md/dir>/.env`.") do |v|
|
121
|
+
options[:dotenv_path] = v
|
122
|
+
end
|
123
|
+
|
124
|
+
opts.on("--output-filename <name>", "Name of the generated markdown file i.e. `#{CLI::DEFAULTS::OUTPUT_FILENAME}`.") do |v|
|
125
|
+
options[:output_filename] = v
|
126
|
+
end
|
127
|
+
|
128
|
+
opts.on("--screenshots-dirname <name>", "Name of screenshot dir i.e. `#{CLI::DEFAULTS::SCREENSHOTS_DIR}`.") do |v|
|
129
|
+
options[:screenshots_dirname] = v
|
130
|
+
end
|
131
|
+
|
132
|
+
opts.on("--force", "Delete contents of the success/failure dirs even if they're not empty.") do |v|
|
133
|
+
options[:force] = v
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
require_relative "cli"
|
@@ -40,8 +40,15 @@ class Rundoc::CodeCommand::Bash < Rundoc::CodeCommand
|
|
40
40
|
IO.popen(cmd, "w+") do |io|
|
41
41
|
io << stdin if stdin
|
42
42
|
io.close_write
|
43
|
-
|
43
|
+
|
44
|
+
until io.eof?
|
45
|
+
buffer = io.gets
|
46
|
+
puts " #{buffer}"
|
47
|
+
|
48
|
+
result << sanitize_escape_chars(buffer)
|
49
|
+
end
|
44
50
|
end
|
51
|
+
|
45
52
|
unless $?.success?
|
46
53
|
raise "Command `#{@line}` exited with non zero status: #{result}" unless keyword.to_s.include?("fail")
|
47
54
|
end
|
@@ -4,30 +4,7 @@ class ::Rundoc::CodeCommand
|
|
4
4
|
# Pass in the relative path of another rundoc document in order to
|
5
5
|
# run all of it's commands (but not to )
|
6
6
|
def initialize(path)
|
7
|
-
raise "
|
8
|
-
@path = Pathname.new(path)
|
9
|
-
end
|
10
|
-
|
11
|
-
def to_md(env = {})
|
12
|
-
""
|
13
|
-
end
|
14
|
-
|
15
|
-
def call(env = {})
|
16
|
-
current_path = Pathname.new(env[:document_path]).dirname
|
17
|
-
document_path = @path.expand_path(current_path)
|
18
|
-
# Run the commands, but do not
|
19
|
-
puts "rundoc.depend_on: Start executing #{@path.to_s.inspect}"
|
20
|
-
output = Rundoc::Parser.new(document_path.read, document_path: document_path.to_s).to_md
|
21
|
-
puts "rundoc.depend_on: Done executing #{@path.to_s.inspect}, discarding intermediate document"
|
22
|
-
output
|
23
|
-
end
|
24
|
-
|
25
|
-
def hidden?
|
26
|
-
true
|
27
|
-
end
|
28
|
-
|
29
|
-
def not_hidden?
|
30
|
-
!hidden?
|
7
|
+
raise "rundoc.depend_on has been removed, use `:::-- rundoc.require` instead"
|
31
8
|
end
|
32
9
|
end
|
33
10
|
end
|
@@ -14,20 +14,26 @@ class ::Rundoc::CodeCommand
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def call(env = {})
|
17
|
-
env[:
|
18
|
-
current_path = Pathname.new(env[:document_path]).dirname
|
19
|
-
document_path = @path.expand_path(current_path)
|
17
|
+
document_path = @path.expand_path(env[:context].source_dir)
|
20
18
|
|
21
19
|
puts "rundoc.require: Start executing #{@path.to_s.inspect}"
|
22
|
-
output = Rundoc::Parser.new(
|
23
|
-
|
20
|
+
output = Rundoc::Parser.new(
|
21
|
+
document_path.read,
|
22
|
+
context: env[:context]
|
23
|
+
).to_md
|
24
|
+
|
25
|
+
if render_result?
|
26
|
+
puts "rundoc.require: Done executing #{@path.to_s.inspect}, putting contents into document"
|
27
|
+
env[:before] << output
|
28
|
+
else
|
29
|
+
puts "rundoc.require: Done executing #{@path.to_s.inspect}, quietly"
|
30
|
+
end
|
24
31
|
|
25
|
-
env[:replace] << output
|
26
32
|
""
|
27
33
|
end
|
28
34
|
|
29
35
|
def hidden?
|
30
|
-
|
36
|
+
!render_result?
|
31
37
|
end
|
32
38
|
|
33
39
|
def not_hidden?
|
@@ -48,23 +48,25 @@ class Rundoc::CodeCommand::Website
|
|
48
48
|
attr_reader :tasks
|
49
49
|
end
|
50
50
|
|
51
|
-
def safe_eval(code)
|
51
|
+
def safe_eval(code, env = {})
|
52
52
|
@driver.send(:eval, code)
|
53
53
|
rescue => e
|
54
54
|
msg = +""
|
55
55
|
msg << "Error running code #{code.inspect} at #{current_url.inspect}\n"
|
56
56
|
msg << "saving a screenshot to: `tmp/error.png`"
|
57
57
|
puts msg
|
58
|
-
|
58
|
+
error_path = env[:context].screenshots_dir.join("error.png")
|
59
|
+
session.save_screenshot(error_path)
|
59
60
|
raise e
|
60
61
|
end
|
61
62
|
|
62
|
-
def screenshot(upload: false)
|
63
|
+
def screenshot(screenshots_dir:, upload: false)
|
63
64
|
@driver.resize_window_to(@driver.current_window_handle, @width, @height)
|
64
|
-
FileUtils.mkdir_p(
|
65
|
+
FileUtils.mkdir_p(screenshots_dir)
|
65
66
|
file_name = self.class.next_screenshot_name
|
66
|
-
file_path =
|
67
|
+
file_path = screenshots_dir.join(file_name)
|
67
68
|
session.save_screenshot(file_path)
|
69
|
+
puts "Screenshot saved to #{file_path}"
|
68
70
|
|
69
71
|
return file_path unless upload
|
70
72
|
|
@@ -11,8 +11,13 @@ class Rundoc::CodeCommand::Website
|
|
11
11
|
|
12
12
|
def call(env = {})
|
13
13
|
puts "Taking screenshot: #{@driver.current_url}"
|
14
|
-
filename = @driver.screenshot(
|
15
|
-
|
14
|
+
filename = @driver.screenshot(
|
15
|
+
upload: @upload,
|
16
|
+
screenshots_dir: env[:context].screenshots_dir
|
17
|
+
)
|
18
|
+
|
19
|
+
relative_filename = filename.relative_path_from(env[:context].output_dir)
|
20
|
+
env[:before] << "![Screenshot of #{@driver.current_url}](#{relative_filename})"
|
16
21
|
""
|
17
22
|
end
|
18
23
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Rundoc::CodeCommand::Website
|
2
4
|
class Visit < Rundoc::CodeCommand
|
3
5
|
def initialize(name:, url: nil, scroll: nil, height: 720, width: 1024, visible: false)
|
@@ -19,7 +21,7 @@ class Rundoc::CodeCommand::Website
|
|
19
21
|
end
|
20
22
|
|
21
23
|
def call(env = {})
|
22
|
-
message =
|
24
|
+
message = "Visting: #{@url}"
|
23
25
|
message << "and executing:\n#{contents}" unless contents.nil? || contents.empty?
|
24
26
|
|
25
27
|
puts message
|
@@ -28,7 +30,7 @@ class Rundoc::CodeCommand::Website
|
|
28
30
|
@driver.scroll(@scroll) if @scroll
|
29
31
|
|
30
32
|
return "" if contents.nil? || contents.empty?
|
31
|
-
@driver.safe_eval(contents)
|
33
|
+
@driver.safe_eval(contents, env)
|
32
34
|
|
33
35
|
""
|
34
36
|
end
|
data/lib/rundoc/code_section.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rundoc
|
2
4
|
# holds code, parses and creates CodeCommand
|
3
5
|
class CodeSection
|
@@ -28,12 +30,12 @@ module Rundoc
|
|
28
30
|
AUTOGEN_WARNING = "\n<!-- STOP. This document is autogenerated. Do not manually modify. See the top of the doc for more details. -->"
|
29
31
|
attr_accessor :original, :fence, :lang, :code, :commands, :keyword
|
30
32
|
|
31
|
-
def initialize(match,
|
33
|
+
def initialize(match, keyword:, context:)
|
32
34
|
@original = match.to_s
|
33
35
|
@commands = []
|
34
36
|
@stack = []
|
35
|
-
@keyword =
|
36
|
-
@
|
37
|
+
@keyword = keyword
|
38
|
+
@context = context
|
37
39
|
@fence = match[:fence]
|
38
40
|
@lang = match[:lang]
|
39
41
|
@code = match[:contents]
|
@@ -44,11 +46,11 @@ module Rundoc
|
|
44
46
|
result = []
|
45
47
|
env = {}
|
46
48
|
env[:commands] = []
|
47
|
-
env[:fence_start] =
|
49
|
+
env[:fence_start] = "#{fence}#{lang}"
|
48
50
|
env[:fence_end] = "#{fence}#{AUTOGEN_WARNING}"
|
49
51
|
env[:before] = []
|
50
52
|
env[:after] = []
|
51
|
-
env[:
|
53
|
+
env[:context] = @context
|
52
54
|
|
53
55
|
@stack.each do |s|
|
54
56
|
unless s.respond_to?(:call)
|
@@ -69,8 +71,6 @@ module Rundoc
|
|
69
71
|
result << tmp_result unless code_command.hidden?
|
70
72
|
end
|
71
73
|
|
72
|
-
return env[:replace] if env[:replace]
|
73
|
-
|
74
74
|
return "" if hidden?
|
75
75
|
|
76
76
|
array = [env[:before]]
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Rundoc
|
2
|
+
module Context
|
3
|
+
# Public interface for the `Rundoc.after_build` proc
|
4
|
+
class AfterBuild
|
5
|
+
attr_reader :output_markdown_path, :screenshots_dir, :output_dir
|
6
|
+
|
7
|
+
def initialize(output_markdown_path:, screenshots_dir:, output_dir:)
|
8
|
+
@output_dir = output_dir
|
9
|
+
@screenshots_dir = screenshots_dir
|
10
|
+
@output_markdown_path = output_markdown_path
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Rundoc
|
2
|
+
module Context
|
3
|
+
# Holds configuration for the currently executing script
|
4
|
+
class Execution
|
5
|
+
# The path to the source file
|
6
|
+
attr_reader :source_path,
|
7
|
+
# The directory containing the source file
|
8
|
+
:source_dir,
|
9
|
+
# The directory we are actively manipulating
|
10
|
+
:output_dir,
|
11
|
+
# Directory to store screenshots, relative to output_dir
|
12
|
+
:screenshots_dir
|
13
|
+
|
14
|
+
def initialize(source_path:, output_dir:, screenshots_dirname:)
|
15
|
+
@source_path = Pathname(source_path).expand_path
|
16
|
+
@source_dir = @source_path.parent
|
17
|
+
@output_dir = Pathname(output_dir).expand_path
|
18
|
+
@screenshots_dir = @output_dir.join(screenshots_dirname).expand_path
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/rundoc/parser.rb
CHANGED
@@ -8,10 +8,10 @@ module Rundoc
|
|
8
8
|
/^#{keyword}(?<tag>(\s|=|-|>)?(=|-|>)?)\s*(?<command>(\S)+)\s+(?<statement>.*)$/
|
9
9
|
}
|
10
10
|
|
11
|
-
attr_reader :contents, :keyword, :stack
|
11
|
+
attr_reader :contents, :keyword, :stack, :context
|
12
12
|
|
13
|
-
def initialize(contents, keyword: DEFAULT_KEYWORD
|
14
|
-
@
|
13
|
+
def initialize(contents, context:, keyword: DEFAULT_KEYWORD)
|
14
|
+
@context = context
|
15
15
|
@contents = contents
|
16
16
|
@original = contents.dup
|
17
17
|
@keyword = keyword
|
@@ -41,7 +41,11 @@ module Rundoc
|
|
41
41
|
@stack << head unless head.empty?
|
42
42
|
unless code.empty?
|
43
43
|
match = code.match(CODEBLOCK_REGEX)
|
44
|
-
@stack << CodeSection.new(
|
44
|
+
@stack << CodeSection.new(
|
45
|
+
match,
|
46
|
+
keyword: keyword,
|
47
|
+
context: context
|
48
|
+
)
|
45
49
|
end
|
46
50
|
@contents = tail
|
47
51
|
end
|
data/lib/rundoc/version.rb
CHANGED
data/lib/rundoc.rb
CHANGED
@@ -48,9 +48,9 @@ module Rundoc
|
|
48
48
|
yield self
|
49
49
|
end
|
50
50
|
|
51
|
-
def run_after_build
|
51
|
+
def run_after_build(context)
|
52
52
|
@after_build_block ||= []
|
53
|
-
@after_build_block.each(
|
53
|
+
@after_build_block.each { |block| block.call(context) }
|
54
54
|
end
|
55
55
|
|
56
56
|
def after_build(&block)
|
@@ -68,7 +68,7 @@ module Rundoc
|
|
68
68
|
@sensitive.merge!(sensitive)
|
69
69
|
end
|
70
70
|
|
71
|
-
def sanitize(doc)
|
71
|
+
def sanitize!(doc)
|
72
72
|
return doc if @sensitive.nil?
|
73
73
|
@sensitive.each do |sensitive, replace|
|
74
74
|
doc.gsub!(sensitive.to_s, replace)
|
@@ -76,11 +76,19 @@ module Rundoc
|
|
76
76
|
doc
|
77
77
|
end
|
78
78
|
|
79
|
-
|
79
|
+
attr_reader :project_root
|
80
|
+
|
81
|
+
def project_root=(root)
|
82
|
+
raise <<~MSG
|
83
|
+
Setting Rundoc.project_root is now a no-op
|
84
|
+
|
85
|
+
If you want to manipulate the directory structure, use `Rundoc.after_build` instead.
|
86
|
+
MSG
|
87
|
+
end
|
80
88
|
end
|
81
89
|
|
82
90
|
require "rundoc/parser"
|
83
91
|
require "rundoc/code_section"
|
84
92
|
require "rundoc/code_command"
|
85
93
|
require "rundoc/peg_parser"
|
86
|
-
require "rundoc/
|
94
|
+
require "rundoc/cli_argument_parser"
|
data/rundoc.gemspec
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
## Download an example Ruby on Rails application
|
2
|
+
|
3
|
+
How do you configure a CNB? Give them an application. While Dockerfile is procedural, buildpacks, are declarative. A buildpack will determine what your application needs to function by inspecting the code on disk.
|
4
|
+
|
5
|
+
For this example, we're using a pre-built Ruby on Rails application. Download it now:
|
6
|
+
|
7
|
+
```
|
8
|
+
:::>- $ git clone https://github.com/heroku/ruby-getting-started
|
9
|
+
:::>- $ cd ruby-getting-started
|
10
|
+
```
|
11
|
+
|
12
|
+
Verify you're in the correct directory:
|
13
|
+
|
14
|
+
```
|
15
|
+
:::>> $ ls
|
16
|
+
```
|
17
|
+
|
18
|
+
This tutorial was built using the following commit SHA:
|
19
|
+
|
20
|
+
```
|
21
|
+
:::>> $ git log --oneline | head -n1
|
22
|
+
```
|
@@ -0,0 +1,34 @@
|
|
1
|
+
## Image structure under the hood
|
2
|
+
|
3
|
+
> [!NOTE]
|
4
|
+
> Skip this section if you want to try building your application with CNBs and learn about container structure later.
|
5
|
+
|
6
|
+
If you’re an advanced `Dockerfile` user you might be interested in learning more about the internal structure of the image on disk. You can access the image disk interactively by using the `bash` docker command above.
|
7
|
+
|
8
|
+
If you view the root directory `/` you’ll see there is a `layers` folder. Every buildpack that executes gets a unique folder. For example:
|
9
|
+
|
10
|
+
```
|
11
|
+
:::>> $ docker run --rm my-image-name "ls /layers | grep ruby"
|
12
|
+
```
|
13
|
+
|
14
|
+
Individual buildpacks can compose multiple layers from their buildpack directory. For example you can see that `ruby` binary is present within that ruby buildpack directory:
|
15
|
+
|
16
|
+
```
|
17
|
+
:::>> $ docker run --rm my-image-name "which ruby"
|
18
|
+
```
|
19
|
+
|
20
|
+
OCI images are represented as sequential modifications to disk. By scoping buildpack disk modifications to their own directory, the CNB API guarantees that changes to a layer in one buildpack will not affect the contents of disk to another layer. This means that OCI images produced by CNBs are rebaseable by default, while those produced by Dockerfile are not.
|
21
|
+
|
22
|
+
We saw before how the image booted a web server by default. This is accomplished using an entrypoint. In another terminal outside of the running container you can view that entrypoint:
|
23
|
+
|
24
|
+
```
|
25
|
+
:::>> $ docker inspect my-image-name | grep '"Entrypoint": \[' -A2
|
26
|
+
```
|
27
|
+
|
28
|
+
From within the image, you can see that file on disk:
|
29
|
+
|
30
|
+
```
|
31
|
+
:::>> $ docker run --rm my-image-name "ls /cnb/process/"
|
32
|
+
```
|
33
|
+
|
34
|
+
While you might not need this level of detail to build and run an application with Cloud Native Buildpacks, it is useful to understand how they’re structured if you ever want to write your own buildpack.
|
@@ -0,0 +1,5 @@
|
|
1
|
+
# Heroku Ruby Cloud Native Buildpack (CNB) Tutorial
|
2
|
+
|
3
|
+
Build a Ruby on Rails application image in 5 minutes, no Dockerfile required.
|
4
|
+
|
5
|
+
At the end of this tutorial, you'll have a working [OCI image](https://opencontainers.org/) of a Ruby on Rails application that can run locally. You will learn about the Cloud Native Buildpack (CNB) ecosystem, and how to utilize the [pack CLI](https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/) to build images without the need to write or maintain a Dockerfile.
|
@@ -0,0 +1,43 @@
|
|
1
|
+
## Configuring multiple languages
|
2
|
+
|
3
|
+
Language support is provided by individual buildpacks that are shipped with the builder. The above example uses the `heroku/ruby` buildpack which is [visible on GitHub](https://github.com/heroku/buildpacks-ruby). When you execute `pack build` with a builder, every buildpack has the opportunity to "detect" if it should execute against that project. The `heroku/ruby` buildpack looks for a `Gemfile.lock` in the root of the project and if found, knows how to detect a Ruby version and install dependencies.
|
4
|
+
|
5
|
+
In addition to this auto-detection behavior, you can specify buildpacks through the `--buildpack` flag with the `pack` CLI or through a [project.toml](https://buildpacks.io/docs/for-app-developers/how-to/build-inputs/specify-buildpacks/) file at the root of your application.
|
6
|
+
|
7
|
+
For example, if you wanted to install both Ruby, NodeJS and Python you could create a `project.toml` file in the root of your application and specify those buildpacks.
|
8
|
+
|
9
|
+
```toml
|
10
|
+
:::>> file.write project.toml
|
11
|
+
[_]
|
12
|
+
schema-version = "0.2"
|
13
|
+
id = "sample.ruby+python.app"
|
14
|
+
name = "Sample Ruby & Python App"
|
15
|
+
version = "1.0.0"
|
16
|
+
|
17
|
+
[[io.buildpacks.group]]
|
18
|
+
uri = "heroku/python"
|
19
|
+
|
20
|
+
[[io.buildpacks.group]]
|
21
|
+
uri = "heroku/nodejs"
|
22
|
+
|
23
|
+
[[io.buildpacks.group]]
|
24
|
+
uri = "heroku/ruby"
|
25
|
+
|
26
|
+
[[io.buildpacks.group]]
|
27
|
+
uri = "heroku/procfile"
|
28
|
+
```
|
29
|
+
|
30
|
+
Ensure that a `requirements.txt` file, a `package.json` file and a `Gemfile.lock` file all exist and then build your application:
|
31
|
+
|
32
|
+
```
|
33
|
+
:::>> $ touch requirements.txt
|
34
|
+
:::>> $ pack build my-image-name --path .
|
35
|
+
```
|
36
|
+
|
37
|
+
You can run the image and inspect everything is installed as expected:
|
38
|
+
|
39
|
+
```
|
40
|
+
$ docker run -it --rm my-image-name bash
|
41
|
+
$ which python
|
42
|
+
:::-> $ docker run --rm my-image-name "which python"
|
43
|
+
```
|
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
```
|
3
|
+
:::>> rundoc.require "./intro.md"
|
4
|
+
```
|
5
|
+
|
6
|
+
```
|
7
|
+
:::>> rundoc.require "../shared/install_pack.md"
|
8
|
+
```
|
9
|
+
|
10
|
+
```
|
11
|
+
:::>> rundoc.require "../shared/configure_builder.md"
|
12
|
+
```
|
13
|
+
|
14
|
+
```
|
15
|
+
:::>> rundoc.require "../shared/what_is_a_builder.md"
|
16
|
+
```
|
17
|
+
|
18
|
+
```
|
19
|
+
:::>> rundoc.require "./download.md"
|
20
|
+
```
|
21
|
+
|
22
|
+
```
|
23
|
+
:::>> rundoc.require "../shared/pack_build.md"
|
24
|
+
```
|
25
|
+
|
26
|
+
```
|
27
|
+
:::>> rundoc.require "./what_is_pack_build.md"
|
28
|
+
```
|
29
|
+
|
30
|
+
```
|
31
|
+
:::>> rundoc.require "../shared/use_the_image.md"
|
32
|
+
```
|
33
|
+
|
34
|
+
```
|
35
|
+
:::>> rundoc.require "./image_structure.md"
|
36
|
+
```
|
37
|
+
|
38
|
+
```
|
39
|
+
:::>> rundoc.require "../shared/call_to_action.md"
|
40
|
+
```
|
41
|
+
|
42
|
+
```
|
43
|
+
:::>> rundoc.require "./multiple_langs.md"
|
44
|
+
```
|
45
|
+
|
46
|
+
```
|
47
|
+
:::>> rundoc.require "../shared/procfile.md"
|
48
|
+
```
|
@@ -0,0 +1,18 @@
|
|
1
|
+
## What does `pack build` do?
|
2
|
+
|
3
|
+
> [!NOTE]
|
4
|
+
> Skip ahead if you want to run the application first and get into the details later.
|
5
|
+
|
6
|
+
When you run `pack build` with a builder, each buildpack runs a detection script to determine if it should be eligible to build the application. In our case the `heroku/ruby` buildpack found a `Gemfile.lock` file and `heroku/nodejs-engine` buildpack found a `package.json` file on disk. As a result, both buildpacks have enough information to install Ruby and Node dependencies. You can view a list of the buildpacks used in the output above:
|
7
|
+
|
8
|
+
```
|
9
|
+
:::-> $ grep DETECTING -A5 ./build_output.txt
|
10
|
+
```
|
11
|
+
|
12
|
+
After the detect phase, each buildpack will execute. Buildpacks can inspect your project, install files to disk, run commands, write environment variables, [and more](https://buildpacks.io/docs/for-buildpack-authors/). You can see some examples of that in the output above. For example, the Ruby buildpack installs dependencies from the `Gemfile` automatically:
|
13
|
+
|
14
|
+
```
|
15
|
+
:::-> $ grep "bundle install" -m1 -A10 ./build_output.txt
|
16
|
+
```
|
17
|
+
|
18
|
+
If you’re familiar with Dockerfile you might know that [many commands in a Dockerfile will create a layer](https://dockerlabs.collabnix.com/beginners/dockerfile/Layering-Dockerfile.html). Buildpacks also use layers, but the CNB buildpack API provides for fine grained control over what exactly is in these layers and how they’re composed. Unlike Dockerfile, all images produced by CNBs [can be rebased](https://tag-env-sustainability.cncf.io/blog/2023-12-reduce-reuse-rebase-buildpacks/#reduce-reuserebase). The CNB api also improves on many of the pitfalls outlined in the satirical article [Write a Good Dockerfile in 19 'Easy' Steps](https://jkutner.github.io/2021/04/26/write-good-dockerfile.html).
|
@@ -0,0 +1,11 @@
|
|
1
|
+
## Try CNBs out on your application
|
2
|
+
|
3
|
+
So far we've learned that CNBs are a declarative interface for producing OCI images (like docker). They aim to be no to low configuration and once built, you can interact with them like any other image.
|
4
|
+
|
5
|
+
For the next step, we encourage you to try running `pack` with the Heroku builder against your application and let us know how it went. We encourage you to share your experience by [opening a discussion](https://github.com/heroku/buildpacks/discussions) and walking us through what happened:
|
6
|
+
|
7
|
+
- What went well?
|
8
|
+
- What could be better?
|
9
|
+
- Do you have any questions?
|
10
|
+
|
11
|
+
We are actively working on our Cloud Native Buildpacks and want to hear about your experience. The documentation below covers some intermediate-level topics that you might find helpful.
|