Class: Dash::Dockerfile::Context

Inherits:
Object
  • Object
show all
Defined in:
lib/dash/dockerfile/context.rb

Overview

Everything a rule is allowed to look at: the parsed Dockerfile, the build context on disk, the builder's configuration, and — when the advice is printed next to a build that actually ran — what that build measured.

The shared predicates live here rather than in each rule so that "what counts as a dependency install" has one answer, and so the measured half of a rule can find the buildx vertex that corresponds to a Dockerfile line.

Constant Summary collapse

DEPENDENCY_INSTALLS =

The package managers whose install step is worth protecting from cache busting, and the cache directory each conventionally wants mounted.

[
  [ /\bbundle\s+(?:_[\d._]+_\s+)?install\b/, "/usr/local/bundle/cache" ],
  [ /\bnpm\s+(?:ci|install)\b/, "/root/.npm" ],
  [ /\byarn\s+install\b/, "/usr/local/share/.cache/yarn" ],
  [ /\bpnpm\s+install\b/, "/root/.local/share/pnpm/store" ],
  [ /\bbun\s+install\b/, "/root/.bun/install/cache" ],
  [ /\bpip3?\s+install\b/, "/root/.cache/pip" ],
  [ /\bpoetry\s+install\b/, "/root/.cache/pypoetry" ],
  [ /\bgo\s+mod\s+download\b/, "/go/pkg/mod" ],
  [ /\bcargo\s+(?:build|fetch)\b/, "/usr/local/cargo/registry" ],
  [ /\bcomposer\s+install\b/, "/root/.composer/cache" ],
  [ /\bmix\s+deps\.get\b/, "/root/.hex" ],
  [ /\bdotnet\s+restore\b/, "/root/.nuget/packages" ]
].freeze
APT_OPTIONS =

apt takes its options before or after the verb (apt-get -y install, apt-get -t bookworm-backports install), so the verb is found past any of them. A shell separator is never an option or its value, so apt-get -y && install is not an apt install.

/(?:-[^\s;&|]+(?:\s+[^-\s;&|][^\s;&|]*)?\s+)*/
APT_INSTALL =
[ /\bapt-get\s+#{APT_OPTIONS.source}install\b/, "/var/cache/apt" ].freeze
BROAD_SOURCES =

A copy that ships the whole tree, so every commit invalidates it and everything layered on top of it.

[ ".", "./", "*", "/" ].freeze
MINIMUM_STEP_MATCH =

buildx expands ARG and ENV references in the vertex name it prints, so a step's text and the Dockerfile line it came from stop agreeing at the first $…. Match on the longest shared prefix instead, and require enough of it that two unrelated RUNs cannot be confused for each other.

12

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document:, context_dir: nil, dockerignore: nil, build: nil, builder: nil, path: "Dockerfile") ⇒ Context

Returns a new instance of Context.



45
46
47
48
49
50
51
52
# File 'lib/dash/dockerfile/context.rb', line 45

def initialize(document:, context_dir: nil, dockerignore: nil, build: nil, builder: nil, path: "Dockerfile")
  @document = document
  @context_dir = context_dir
  @dockerignore = dockerignore
  @build = build
  @builder = builder
  @path = path
end

Instance Attribute Details

#build ⇒ Object (readonly)

Returns the value of attribute build.



43
44
45
# File 'lib/dash/dockerfile/context.rb', line 43

def build
  @build
end

#builder ⇒ Object (readonly)

Returns the value of attribute builder.



43
44
45
# File 'lib/dash/dockerfile/context.rb', line 43

def builder
  @builder
end

#context_dir ⇒ Object (readonly)

Returns the value of attribute context_dir.



43
44
45
# File 'lib/dash/dockerfile/context.rb', line 43

def context_dir
  @context_dir
end

#dockerignore ⇒ Object (readonly)

Returns the value of attribute dockerignore.



43
44
45
# File 'lib/dash/dockerfile/context.rb', line 43

def dockerignore
  @dockerignore
end

#document ⇒ Object (readonly)

Returns the value of attribute document.



43
44
45
# File 'lib/dash/dockerfile/context.rb', line 43

def document
  @document
end

#path ⇒ Object (readonly)

Returns the value of attribute path.



43
44
45
# File 'lib/dash/dockerfile/context.rb', line 43

def path
  @path
end

Instance Method Details

#apt_install?(instruction) ⇒ Boolean

Returns:

  • (Boolean)


72
73
74
# File 'lib/dash/dockerfile/context.rb', line 72

def apt_install?(instruction)
  instruction.name == "RUN" && instruction.shell_command.match?(APT_INSTALL.first)
end

#broad_copy?(instruction) ⇒ Boolean

Returns:

  • (Boolean)


76
77
78
79
80
81
# File 'lib/dash/dockerfile/context.rb', line 76

def broad_copy?(instruction)
  return false unless %w[ COPY ADD ].include?(instruction.name)
  return false if instruction.flag?("from")

  sources(instruction).any? { |source| BROAD_SOURCES.include?(source) }
end

#build_step_for(instruction) ⇒ Object

buildx labels a step with its stage name except in a single-stage build, where it prints none. A multi-platform build reports the same step once per platform; the slowest one is the number worth quoting.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/dash/dockerfile/context.rb', line 95

def build_step_for(instruction)
  return unless build

  text = normalize(instruction.to_s)
  candidates = build.instruction_steps.select { |step| same_stage?(step, instruction) }

  best = candidates.max_by do |step|
    matched = normalize(step.instruction)
    [ matched == text ? 1 : 0, shared_prefix(text, matched), step.seconds.to_f ]
  end
  return unless best

  matched = normalize(best.instruction)
  best if matched == text || shared_prefix(text, matched) >= MINIMUM_STEP_MATCH
end

#busted_by_broad_copy?(instruction) ⇒ Boolean

A dependency install directly after a broad copy is invalidated by every commit, which is a finding of its own — rules that would otherwise report the same slow step twice defer to it.

Returns:

  • (Boolean)


86
87
88
89
90
# File 'lib/dash/dockerfile/context.rb', line 86

def busted_by_broad_copy?(instruction)
  stage = instruction.stage or return false

  stage.instructions.any? { |other| other.line < instruction.line && broad_copy?(other) }
end

#context_entries(name) ⇒ Object



111
112
113
114
115
# File 'lib/dash/dockerfile/context.rb', line 111

def context_entries(name)
  return [] unless context_dir

  Dir.glob(name, base: context_dir, flags: ::File::FNM_DOTMATCH)
end

#dependency_install?(instruction) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
# File 'lib/dash/dockerfile/context.rb', line 58

def dependency_install?(instruction)
  !install_match(instruction).nil?
end

#install_cache_target(instruction) ⇒ Object



68
69
70
# File 'lib/dash/dockerfile/context.rb', line 68

def install_cache_target(instruction)
  install_match(instruction)&.last
end

#install_command(instruction) ⇒ Object

The command that made it a dependency install ("bundle install"), for advice that names what the operator wrote rather than the whole RUN.



64
65
66
# File 'lib/dash/dockerfile/context.rb', line 64

def install_command(instruction)
  install_match(instruction)&.first
end

#location_for(instruction) ⇒ Object



54
55
56
# File 'lib/dash/dockerfile/context.rb', line 54

def location_for(instruction)
  "#{path}:#{instruction.line}"
end