rundoc 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +10 -7
  3. data/CHANGELOG.md +15 -0
  4. data/CONTRIBUTING.md +25 -0
  5. data/README.md +35 -48
  6. data/Rakefile +5 -1
  7. data/bin/rundoc +4 -17
  8. data/lib/rundoc/cli.rb +207 -49
  9. data/lib/rundoc/cli_argument_parser.rb +140 -0
  10. data/lib/rundoc/code_command/bash.rb +8 -1
  11. data/lib/rundoc/code_command/rundoc/depend_on.rb +1 -24
  12. data/lib/rundoc/code_command/rundoc/require.rb +13 -7
  13. data/lib/rundoc/code_command/website/driver.rb +7 -5
  14. data/lib/rundoc/code_command/website/navigate.rb +1 -1
  15. data/lib/rundoc/code_command/website/screenshot.rb +7 -2
  16. data/lib/rundoc/code_command/website/visit.rb +1 -1
  17. data/lib/rundoc/code_section.rb +4 -6
  18. data/lib/rundoc/context/after_build.rb +14 -0
  19. data/lib/rundoc/context/execution.rb +22 -0
  20. data/lib/rundoc/parser.rb +9 -5
  21. data/lib/rundoc/version.rb +1 -1
  22. data/lib/rundoc.rb +13 -5
  23. data/rundoc.gemspec +1 -0
  24. data/test/fixtures/cnb/ruby/download.md +22 -0
  25. data/test/fixtures/cnb/ruby/image_structure.md +34 -0
  26. data/test/fixtures/cnb/ruby/intro.md +5 -0
  27. data/test/fixtures/cnb/ruby/multiple_langs.md +43 -0
  28. data/test/fixtures/cnb/ruby/rundoc.md +48 -0
  29. data/test/fixtures/cnb/ruby/what_is_pack_build.md +18 -0
  30. data/test/fixtures/cnb/shared/call_to_action.md +11 -0
  31. data/test/fixtures/cnb/shared/configure_builder.md +23 -0
  32. data/test/fixtures/cnb/shared/install_pack.md +14 -0
  33. data/test/fixtures/cnb/shared/pack_build.md +20 -0
  34. data/test/fixtures/cnb/shared/procfile.md +13 -0
  35. data/test/fixtures/cnb/shared/use_the_image.md +52 -0
  36. data/test/fixtures/cnb/shared/what_is_a_builder.md +18 -0
  37. data/test/fixtures/rails_4/rundoc.md +5 -7
  38. data/test/fixtures/rails_5/rundoc.md +1 -1
  39. data/test/fixtures/rails_6/rundoc.md +2 -2
  40. data/test/fixtures/rails_7/rundoc.md +2 -3
  41. data/test/fixtures/simple_git/rundoc.md +13 -0
  42. data/test/integration/after_build_test.rb +62 -0
  43. data/test/integration/print_test.rb +9 -9
  44. data/test/integration/require_test.rb +63 -0
  45. data/test/integration/website_test.rb +35 -0
  46. data/test/rundoc/cli_argument_parser_test.rb +118 -0
  47. data/test/rundoc/code_section_test.rb +40 -8
  48. data/test/rundoc/parser_test.rb +3 -3
  49. data/test/rundoc/peg_parser_test.rb +6 -6
  50. data/test/rundoc/regex_test.rb +22 -0
  51. data/test/system/exe_cli_test.rb +231 -0
  52. data/test/test_helper.rb +74 -1
  53. metadata +40 -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
- result = sanitize_escape_chars io.read
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 "Path must be relative (i.e. start with `.` or `..`. #{path.inspect} does not" unless path.start_with?(".")
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[:replace] ||= +""
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(document_path.read, document_path: document_path.to_s).to_md
23
- puts "rundoc.require: Done executing #{@path.to_s.inspect}, putting contents into document"
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
- true
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
- session.save_screenshot("tmp/error.png")
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("tmp/rundoc_screenshots")
65
+ FileUtils.mkdir_p(screenshots_dir)
65
66
  file_name = self.class.next_screenshot_name
66
- file_path = "tmp/rundoc_screenshots/#{file_name}"
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,7 +11,7 @@ class Rundoc::CodeCommand::Website
11
11
 
12
12
  def call(env = {})
13
13
  puts "website.navigate [#{@name}]: #{contents}"
14
- @driver.safe_eval(contents)
14
+ @driver.safe_eval(contents, env)
15
15
  ""
16
16
  end
17
17
 
@@ -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(upload: @upload)
15
- env[:replace] = "![Screenshot of #{@driver.current_url}](#{filename})"
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
 
@@ -28,7 +28,7 @@ class Rundoc::CodeCommand::Website
28
28
  @driver.scroll(@scroll) if @scroll
29
29
 
30
30
  return "" if contents.nil? || contents.empty?
31
- @driver.safe_eval(contents)
31
+ @driver.safe_eval(contents, env)
32
32
 
33
33
  ""
34
34
  end
@@ -28,12 +28,12 @@ module Rundoc
28
28
  AUTOGEN_WARNING = "\n<!-- STOP. This document is autogenerated. Do not manually modify. See the top of the doc for more details. -->"
29
29
  attr_accessor :original, :fence, :lang, :code, :commands, :keyword
30
30
 
31
- def initialize(match, options = {})
31
+ def initialize(match, keyword:, context:)
32
32
  @original = match.to_s
33
33
  @commands = []
34
34
  @stack = []
35
- @keyword = options[:keyword] or raise "keyword is required"
36
- @document_path = options[:document_path]
35
+ @keyword = keyword
36
+ @context = context
37
37
  @fence = match[:fence]
38
38
  @lang = match[:lang]
39
39
  @code = match[:contents]
@@ -48,7 +48,7 @@ module Rundoc
48
48
  env[:fence_end] = "#{fence}#{AUTOGEN_WARNING}"
49
49
  env[:before] = []
50
50
  env[:after] = []
51
- env[:document_path] = @document_path
51
+ env[:context] = @context
52
52
 
53
53
  @stack.each do |s|
54
54
  unless s.respond_to?(:call)
@@ -69,8 +69,6 @@ module Rundoc
69
69
  result << tmp_result unless code_command.hidden?
70
70
  end
71
71
 
72
- return env[:replace] if env[:replace]
73
-
74
72
  return "" if hidden?
75
73
 
76
74
  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
@@ -2,16 +2,16 @@ module Rundoc
2
2
  class Parser
3
3
  DEFAULT_KEYWORD = ":::"
4
4
  INDENT_BLOCK = '(?<before_indent>(^\s*$\n|\A)(^(?:[ ]{4}|\t))(?<indent_contents>.*)(?<after_indent>[^\s].*$\n?(?:(?:^\s*$\n?)*^(?:[ ]{4}|\t).*[^\s].*$\n?)*))'
5
- GITHUB_BLOCK = '^(?<fence>(?<fence_char>~|`){3,})\s*?(?<lang>\w+)?\s*?\n(?<contents>.*?)^\g<fence>\g<fence_char>*\s*?\n'
5
+ GITHUB_BLOCK = '^(?<fence>(?<fence_char>~|`){3,})\s*?(?<lang>\w+)?\s*?\n(?<contents>.*?)^\g<fence>\g<fence_char>*\s*?\n?'
6
6
  CODEBLOCK_REGEX = /(#{GITHUB_BLOCK})/m
7
7
  COMMAND_REGEX = ->(keyword) {
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, document_path: nil)
14
- @document_path = document_path
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(match, keyword: keyword, document_path: @document_path)
44
+ @stack << CodeSection.new(
45
+ match,
46
+ keyword: keyword,
47
+ context: context
48
+ )
45
49
  end
46
50
  @contents = tail
47
51
  end
@@ -1,3 +1,3 @@
1
1
  module Rundoc
2
- VERSION = "2.0.0"
2
+ VERSION = "3.0.0"
3
3
  end
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(&:call)
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
- attr_accessor :project_root
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/cli"
94
+ require "rundoc/cli_argument_parser"
data/rundoc.gemspec CHANGED
@@ -29,4 +29,5 @@ Gem::Specification.new do |gem|
29
29
  gem.add_development_dependency "mocha"
30
30
  gem.add_development_dependency "minitest"
31
31
  gem.add_development_dependency "standard"
32
+ gem.add_development_dependency "simplecov"
32
33
  end
@@ -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.
@@ -0,0 +1,23 @@
1
+ ## Configure the default pack builder
2
+
3
+ Once `pack` is installed, the only configuration you'll need for this tutorial is to set a default builder:
4
+
5
+ ```
6
+ :::>> $ pack config default-builder heroku/builder:22
7
+ ```
8
+
9
+ You can view your default builder at any time:
10
+
11
+ ```
12
+ :::>> $ pack config default-builder
13
+ ```
14
+
15
+ The following tutorial is built on amd64 architecture (also known as x86). If you are building on a machine with different architecture (such as arm64/aarch64 for a Mac) you will need to tell Docker to use `linux/amd64` architecture. You can do this via a `--platform linux/amd64` flag or by exporting an environment variable:
16
+
17
+ ```
18
+ $ export DOCKER_DEFAULT_PLATFORM=linux/amd64
19
+ :::-- rundoc.configure
20
+ # Needed because all `$` commands are run as separate isolated processes
21
+
22
+ ENV["DOCKER_DEFAULT_PLATFORM"] = "linux/amd64"
23
+ ```