Rakefile 19.2 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- mode: ruby -*-
# vi: set ft=ruby :
#
# Tails: The Amnesic Incognito Live System
# Copyright © 2012 Tails developers <tails@boum.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

21
require 'date'
22
require 'libvirt'
anonym's avatar
anonym committed
23
require 'open3'
24
require 'rbconfig'
25
require 'uri'
26

27
require_relative 'vagrant/lib/tails_build_settings'
28

29
30
31
# Path to the directory which holds our Vagrantfile
VAGRANT_PATH = File.expand_path('../vagrant', __FILE__)

32
33
34
# Branches that are considered 'stable' (used to select SquashFS compression)
STABLE_BRANCH_NAMES = ['stable', 'testing']

35
36
EXPORTED_VARIABLES = [
  'MKSQUASHFS_OPTIONS',
37
  'TAILS_BUILD_FAILURE_RESCUE',
38
  'TAILS_DATE_OFFSET',
39
  'TAILS_MERGE_BASE_BRANCH',
anonym's avatar
anonym committed
40
  'TAILS_OFFLINE_MODE',
41
42
43
  'TAILS_PROXY',
  'TAILS_PROXY_TYPE',
  'TAILS_RAM_BUILD',
bertagaz's avatar
bertagaz committed
44
45
46
  'GIT_COMMIT',
  'GIT_REF',
  'BASE_BRANCH_GIT_COMMIT',
47
]
48
ENV['EXPORTED_VARIABLES'] = EXPORTED_VARIABLES.join(' ')
Tails developers's avatar
Tails developers committed
49

50
51
EXTERNAL_HTTP_PROXY = ENV['http_proxy']

52
# In-VM proxy URL
anonym's avatar
anonym committed
53
INTERNAL_HTTP_PROXY = "http://#{VIRTUAL_MACHINE_HOSTNAME}:3142"
54

55
56
ENV['ARTIFACTS'] ||= '.'

57
class CommandError < StandardError
anonym's avatar
anonym committed
58
59
60
61
62
63
64
65
  attr_reader :status, :stderr

  def initialize(message = nil, opts = {})
    opts[:status] ||= nil
    opts[:stderr] ||= nil
    @status = opts[:status]
    @stderr = opts[:stderr]
    super(message % {status: @status, stderr: @stderr})
66
67
68
69
70
71
  end
end

def run_command(*args)
  Process.wait Kernel.spawn(*args)
  if $?.exitstatus != 0
anonym's avatar
anonym committed
72
    raise CommandError.new("command #{args} failed with exit status " +
anonym's avatar
anonym committed
73
                           "%{status}", status: $?.exitstatus)
74
75
76
77
  end
end

def capture_command(*args)
anonym's avatar
anonym committed
78
  stdout, stderr, proc_status = Open3.capture3(*args)
79
80
  if proc_status.exitstatus != 0
    raise CommandError.new("command #{args} failed with exit status " +
anonym's avatar
anonym committed
81
82
                           "%{status}: %{stderr}",
                           stderr: stderr, status: proc_status.exitstatus)
83
84
  end
  return stdout, stderr
85
86
end

anonym's avatar
anonym committed
87
def git_helper(*args)
88
89
  question = args.first.end_with?('?')
  args.first.sub!(/\?$/, '')
anonym's avatar
anonym committed
90
91
92
93
94
95
96
  status = 0
  stdout = ''
  begin
    stdout, _ = capture_command('auto/scripts/utils.sh', *args)
  rescue CommandError => e
    status = e.status
  end
97
  if question
anonym's avatar
anonym committed
98
    return status == 0
anonym's avatar
anonym committed
99
  else
100
    return stdout.chomp
anonym's avatar
anonym committed
101
102
103
  end
end

104
class VagrantCommandError < CommandError
105
106
end

anonym's avatar
anonym committed
107
108
# Runs the vagrant command, letting stdout/stderr through. Throws an
# exception unless the vagrant command succeeds.
anonym's avatar
anonym committed
109
def run_vagrant(*args)
anonym's avatar
anonym committed
110
111
  run_command('vagrant', *args, :chdir => './vagrant')
rescue CommandError => e
112
  raise(VagrantCommandError, "'vagrant #{args}' command failed with exit " +
anonym's avatar
anonym committed
113
                             "status #{e.status}")
114
115
end

anonym's avatar
anonym committed
116
# Runs the vagrant command, not letting stdout/stderr through, and
anonym's avatar
anonym committed
117
# returns [stdout, stderr, Preocess:Status].
anonym's avatar
anonym committed
118
def capture_vagrant(*args)
anonym's avatar
anonym committed
119
  capture_command('vagrant', *args, :chdir => './vagrant')
120
121
rescue CommandError => e
  raise(VagrantCommandError, "'vagrant #{args}' command failed with exit " +
anonym's avatar
anonym committed
122
                             "status #{e.status}: #{e.stderr}")
123
124
end

anonym's avatar
anonym committed
125
126
[:run_vagrant, :capture_vagrant].each do |m|
  define_method "#{m}_ssh" do |*args|
anonym's avatar
anonym committed
127
    method(m).call('ssh', '-c', *args, '--', '-q')
128
  end
129
130
end

anonym's avatar
anonym committed
131
def vagrant_ssh_config(key)
anonym's avatar
anonym committed
132
  # Cache results
anonym's avatar
anonym committed
133
134
135
136
137
138
139
140
141
142
  if $vagrant_ssh_config.nil?
    $vagrant_ssh_config = capture_vagrant('ssh-config').first.split("\n") \
                           .map { |line| line.strip.split(/\s+/, 2) } .to_h
    # The path in the ssh-config output is quoted, which is not what
    # is expected outside of a shell, so let's get rid of the quotes.
    $vagrant_ssh_config['IdentityFile'].gsub!(/^"|"$/, '')
  end
  $vagrant_ssh_config[key]
end

anonym's avatar
anonym committed
143
def current_vm_cpus
anonym's avatar
anonym committed
144
  capture_vagrant_ssh('grep -c "^processor\s*:" /proc/cpuinfo').first.chomp.to_i
145
146
end

anonym's avatar
anonym committed
147
def vm_state
148
  out, _ = capture_vagrant('status')
anonym's avatar
anonym committed
149
150
151
152
153
154
155
  status_line = out.split("\n")[2]
  if    status_line['not created']
    return :not_created
  elsif status_line['shutoff']
    return :poweroff
  elsif status_line['running']
    return :running
156
  else
anonym's avatar
anonym committed
157
    raise "could not determine VM state"
158
159
160
  end
end

161
def enough_free_host_memory_for_ram_build?
162
163
164
  return false unless RbConfig::CONFIG['host_os'] =~ /linux/i

  begin
165
    usable_free_mem = `free`.split[12].to_i
166
167
168
169
170
171
    usable_free_mem > VM_MEMORY_FOR_RAM_BUILDS * 1024
  rescue
    false
  end
end

172
def free_vm_memory
173
  capture_vagrant_ssh('free').first.chomp.split[12].to_i
174
175
176
177
178
179
180
end

def enough_free_vm_memory_for_ram_build?
  free_vm_memory > BUILD_SPACE_REQUIREMENT * 1024
end

def enough_free_memory_for_ram_build?
anonym's avatar
anonym committed
181
  if vm_state == :running
182
183
184
185
186
187
    enough_free_vm_memory_for_ram_build?
  else
    enough_free_host_memory_for_ram_build?
  end
end

188
def is_release?
189
  git_helper('git_on_a_tag?')
190
191
end

192
193
194
195
196
197
198
199
200
201
def system_cpus
  return nil unless RbConfig::CONFIG['host_os'] =~ /linux/i

  begin
    File.read('/proc/cpuinfo').scan(/^processor\s+:/).count
  rescue
    nil
  end
end

202
task :parse_build_options do
203
  options = []
204

205
  # Default to in-memory builds if there is enough RAM available
206
  options << 'ram' if enough_free_memory_for_ram_build?
207

208
  # Default to build using the in-VM proxy
209
  options << 'vmproxy'
210

211
  # Default to fast compression on development branches
212
  options << 'gzipcomp' unless is_release?
213

214
215
  # Default to the number of system CPUs when we can figure it out
  cpus = system_cpus
216
  options << "cpus=#{cpus}" if cpus
217

218
  options += ENV['TAILS_BUILD_OPTIONS'].split if ENV['TAILS_BUILD_OPTIONS']
219

220
  options.uniq.each do |opt|
221
    case opt
222
223
224
225
226
    # Memory build settings
    when 'ram'
      ENV['TAILS_RAM_BUILD'] = '1'
    when 'noram'
      ENV['TAILS_RAM_BUILD'] = nil
227
    # Bootstrap cache settings
228
229
230
    # HTTP proxy settings
    when 'extproxy'
      abort "No HTTP proxy set, but one is required by TAILS_BUILD_OPTIONS. Aborting." unless EXTERNAL_HTTP_PROXY
231
      ENV['TAILS_PROXY'] = EXTERNAL_HTTP_PROXY
232
      ENV['TAILS_PROXY_TYPE'] = 'extproxy'
233
    when 'vmproxy'
234
      ENV['TAILS_PROXY'] = INTERNAL_HTTP_PROXY
235
      ENV['TAILS_PROXY_TYPE'] = 'vmproxy'
236
    when 'noproxy'
237
      ENV['TAILS_PROXY'] = nil
238
      ENV['TAILS_PROXY_TYPE'] = 'noproxy'
239
240
    when 'offline'
      ENV['TAILS_OFFLINE_MODE'] = '1'
241
242
    # SquashFS compression settings
    when 'gzipcomp'
243
      ENV['MKSQUASHFS_OPTIONS'] = '-comp gzip -Xcompression-level 1'
244
245
246
      if is_release?
        raise 'We must use the default compression when building releases!'
      end
247
248
    when 'defaultcomp'
      ENV['MKSQUASHFS_OPTIONS'] = nil
249
250
251
    # Virtual hardware settings
    when /machinetype=([a-zA-Z0-9_.-]+)/
      ENV['TAILS_BUILD_MACHINE_TYPE'] = $1
252
253
    when /cpus=(\d+)/
      ENV['TAILS_BUILD_CPUS'] = $1
254
255
    when /cpumodel=([a-zA-Z0-9_-]+)/
      ENV['TAILS_BUILD_CPU_MODEL'] = $1
256
257
258
    # Git settings
    when 'ignorechanges'
      ENV['TAILS_BUILD_IGNORE_CHANGES'] = '1'
259
260
    when /dateoffset=([-+]\d+)/
      ENV['TAILS_DATE_OFFSET'] = $1
261
262
263
    # Developer convenience features
    when 'keeprunning'
      $keep_running = true
264
265
266
267
      $force_cleanup = false
    when 'forcecleanup'
      $force_cleanup = true
      $keep_running = false
268
269
270
    when 'rescue'
      $keep_running = true
      ENV['TAILS_BUILD_FAILURE_RESCUE'] = '1'
271
272
    # Jenkins
    when 'mergebasebranch'
273
      ENV['TAILS_MERGE_BASE_BRANCH'] = '1'
274
275
    else
      raise "Unknown Tails build option '#{opt}'"
276
277
    end
  end
278
279

  if ENV['TAILS_OFFLINE_MODE'] == '1'
280
    if ENV['TAILS_PROXY'].nil?
281
282
283
      abort "You must use a caching proxy when building offline"
    end
  end
284
285
286
end

task :ensure_clean_repository do
287
288
  git_status = `git status --porcelain`
  unless git_status.empty?
289
290
291
    if ENV['TAILS_BUILD_IGNORE_CHANGES']
      $stderr.puts <<-END_OF_MESSAGE.gsub(/^        /, '')

hybridwipe's avatar
hybridwipe committed
292
        You have uncommitted changes in the Git repository. They will
293
294
        be ignored for the upcoming build:
        #{git_status}
295
296
297
298
299

      END_OF_MESSAGE
    else
      $stderr.puts <<-END_OF_MESSAGE.gsub(/^        /, '')

hybridwipe's avatar
hybridwipe committed
300
        You have uncommitted changes in the Git repository. Due to limitations
301
302
        of the build system, you need to commit them before building Tails:
        #{git_status}
303
304
305
306
307
308

        If you don't care about those changes and want to build Tails nonetheless,
        please add `ignorechanges` to the TAILS_BUILD_OPTIONS environment
        variable.

      END_OF_MESSAGE
hybridwipe's avatar
hybridwipe committed
309
      abort 'Uncommitted changes. Aborting.'
310
311
312
313
    end
  end
end

anonym's avatar
anonym committed
314
315
def list_artifacts
  user = vagrant_ssh_config('User')
anonym's avatar
anonym committed
316
  stdout = capture_vagrant_ssh("find '/home/#{user}/amnesia/' -maxdepth 1 " +
317
                                        "-name 'tails-amd64-*'").first
anonym's avatar
anonym committed
318
  stdout.split("\n")
319
320
rescue VagrantCommandError
  return Array.new
anonym's avatar
anonym committed
321
322
323
end

def remove_artifacts
324
  list_artifacts.each do |artifact|
anonym's avatar
anonym committed
325
    run_vagrant_ssh("sudo rm -f '#{artifact}'")
326
  end
anonym's avatar
anonym committed
327
328
end

329
task :ensure_clean_home_directory => ['vm:up'] do
anonym's avatar
anonym committed
330
  remove_artifacts
331
332
end

333
task :validate_http_proxy do
334
335
  if ENV['TAILS_PROXY']
    proxy_host = URI.parse(ENV['TAILS_PROXY']).host
336

337
    if proxy_host.nil?
338
      ENV['TAILS_PROXY'] = nil
339
340
341
342
      $stderr.puts "Ignoring invalid HTTP proxy."
      return
    end

343
344
345
346
    if ['localhost', '[::1]'].include?(proxy_host) || proxy_host.start_with?('127.0.0.')
      abort 'Using an HTTP proxy listening on the loopback is doomed to fail. Aborting.'
    end

347
    $stderr.puts "Using HTTP proxy: #{ENV['TAILS_PROXY']}"
348
349
350
351
352
  else
    $stderr.puts "No HTTP proxy set."
  end
end

353
task :validate_git_state do
354
  if git_helper('git_in_detached_head?') && not(git_helper('git_on_a_tag?'))
355
356
357
358
359
    raise 'We are in detached head but the current commit is not tagged'
  end
end

task :setup_environment => ['validate_git_state'] do
360
  ENV['GIT_COMMIT'] ||= git_helper('git_current_commit')
361
362
363
364
365
366
367
  ENV['GIT_REF'] ||= git_helper('git_current_head_name')
  if on_jenkins?
    jenkins_branch = (ENV['GIT_BRANCH'] || '').sub(/^origin\//, '')
    if not(is_release?) && jenkins_branch != ENV['GIT_REF']
      raise "We expected to build the Git ref '#{ENV['GIT_REF']}', but GIT_REF in the environment says '#{jenkins_branch}'. Aborting!"
    end
  end
368

anonym's avatar
anonym committed
369
  ENV['BASE_BRANCH_GIT_COMMIT'] = git_helper('git_base_branch_head')
bertagaz's avatar
bertagaz committed
370
  ['GIT_COMMIT', 'GIT_REF', 'BASE_BRANCH_GIT_COMMIT'].each do |var|
371
    if ENV[var].empty?
372
373
374
      raise "Variable '#{var}' is empty, which should not be possible: " +
            "either validate_git_state is buggy or the 'origin' remote " +
            "does not point to the official Tails Git repository."
375
376
377
378
    end
  end
end

379
380
task :maybe_clean_up_builder_vms do
  clean_up_builder_vms if $force_cleanup
381
382
end

Tails developers's avatar
Tails developers committed
383
desc 'Build Tails'
384
task :build => ['parse_build_options', 'ensure_clean_repository', 'maybe_clean_up_builder_vms', 'validate_git_state', 'setup_environment', 'validate_http_proxy', 'vm:up', 'ensure_clean_home_directory'] do
385

anonym's avatar
anonym committed
386
387
388
  begin
    if ENV['TAILS_RAM_BUILD'] && not(enough_free_memory_for_ram_build?)
      $stderr.puts <<-END_OF_MESSAGE.gsub(/^        /, '')
389

anonym's avatar
anonym committed
390
391
392
393
        The virtual machine is not currently set with enough memory to
        perform an in-memory build. Either remove the `ram` option from
        the TAILS_BUILD_OPTIONS environment variable, or shut the
        virtual machine down using `rake vm:halt` before trying again.
394

anonym's avatar
anonym committed
395
396
397
      END_OF_MESSAGE
      abort 'Not enough memory for the virtual machine to run an in-memory build. Aborting.'
    end
398

anonym's avatar
anonym committed
399
400
    if ENV['TAILS_BUILD_CPUS'] && current_vm_cpus != ENV['TAILS_BUILD_CPUS'].to_i
      $stderr.puts <<-END_OF_MESSAGE.gsub(/^        /, '')
401

anonym's avatar
anonym committed
402
403
404
405
        The virtual machine is currently running with #{current_vm_cpus}
        virtual CPU(s). In order to change that number, you need to
        stop the VM first, using `rake vm:halt`. Otherwise, please
        adjust the `cpus` options accordingly.
406

anonym's avatar
anonym committed
407
408
409
      END_OF_MESSAGE
      abort 'The virtual machine needs to be reloaded to change the number of CPUs. Aborting.'
    end
410

anonym's avatar
anonym committed
411
412
    exported_env = EXPORTED_VARIABLES.select { |k| ENV[k] }.
                   collect { |k| "#{k}='#{ENV[k]}'" }.join(' ')
anonym's avatar
anonym committed
413
    run_vagrant_ssh("#{exported_env} build-tails")
anonym's avatar
anonym committed
414
415

    artifacts = list_artifacts
anonym's avatar
anonym committed
416
    raise 'No build artifacts were found!' if artifacts.empty?
anonym's avatar
anonym committed
417
418
419
420
    user     = vagrant_ssh_config('User')
    hostname = vagrant_ssh_config('HostName')
    key_file = vagrant_ssh_config('IdentityFile')
    $stderr.puts "Retrieving artifacts from Vagrant build box."
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
    run_vagrant_ssh(
      "sudo chown #{user} " + artifacts.map { |a| "'#{a}'" } .join(' ')
    )
    fetch_command = [
      'scp',
      '-i', key_file,
      # We need this since the user will not necessarily have a
      # known_hosts entry. It is safe since an attacker must
      # compromise libvirt's network config or the user running the
      # command to modify the #{hostname} below.
      '-o', 'StrictHostKeyChecking=no',
      '-o', 'UserKnownHostsFile=/dev/null',
    ]
    fetch_command += artifacts.map { |a| "#{user}@#{hostname}:#{a}" }
    fetch_command << ENV['ARTIFACTS']
    run_command(*fetch_command)
437
    clean_up_builder_vms unless $keep_running
anonym's avatar
anonym committed
438
  ensure
439
    clean_up_builder_vms if $force_cleanup
440
  end
Tails developers's avatar
Tails developers committed
441
442
end

anonym's avatar
anonym committed
443
444
def has_box?
  not(capture_vagrant('box', 'list').grep(/^#{box_name}\s+\(libvirt,/).empty?)
445
end
446

anonym's avatar
anonym committed
447
448
def domain_name
  "#{box_name}_default"
449
end
450

451
def clean_up_builder_vms
452
  $virt = Libvirt::open("qemu:///system")
453
454
455

  clean_up_domain = Proc.new do |domain|
    next if domain.nil?
456
    domain.destroy if domain.active?
457
    domain.undefine
458
459
460
461
462
463
464
465
    begin
      $virt
        .lookup_storage_pool_by_name('default')
        .lookup_volume_by_name("#{domain.name}.img")
        .delete
    rescue Libvirt::RetrieveError
      # Expected if the pool or disk does not exist
    end
466
  end
467

468
469
  # Let's ensure that the VM we are about to create is cleaned up ...
  previous_domain = $virt.list_all_domains.find { |d| d.name == domain_name }
470
  if previous_domain && previous_domain.active?
471
472
473
474
475
476
477
478
479
480
    begin
      run_vagrant_ssh("mountpoint -q /var/cache/apt-cacher-ng")
    rescue VagrantCommandError
    # Nothing to unmount.
    else
      run_vagrant_ssh("sudo systemctl stop apt-cacher-ng.service")
      run_vagrant_ssh("sudo umount /var/cache/apt-cacher-ng")
      run_vagrant_ssh("sudo sync")
    end
  end
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
  clean_up_domain.call(previous_domain)

  # ... and the same for any residual VM based on another box (=>
  # another domain name) that Vagrant still keeps track of.
  old_domain =
    begin
      old_domain_uuid =
        open('vagrant/.vagrant/machines/default/libvirt/id', 'r') { |f| f.read }
        .strip
      $virt.lookup_domain_by_uuid(old_domain_uuid)
    rescue Errno::ENOENT, Libvirt::RetrieveError
      # Expected if we don't have vagrant/.vagrant, or if the VM was
      # undefined for other reasons (e.g. manually).
      nil
    end
  clean_up_domain.call(old_domain)

  # We could use `vagrant destroy` here but due to vagrant-libvirt's
  # upstream issue #746 we then risk losing the apt-cacher-ng data.
  # Since we essentially implement `vagrant destroy` without this bug
intrigeri's avatar
intrigeri committed
501
  # above, but in a way so it works even if `vagrant/.vagrant` does
anonym's avatar
anonym committed
502
  # not exist, let's just do what is safest, i.e. avoiding `vagrant
503
504
505
  # destroy`. For details, see the upstream issue:
  #   https://github.com/vagrant-libvirt/vagrant-libvirt/issues/746
  FileUtils.rm_rf('vagrant/.vagrant')
506
507
ensure
  $virt.close
Tails developers's avatar
Tails developers committed
508
end
509

510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
desc "Remove all libvirt volumes named tails-builder-* (run at your own risk!)"
task :clean_up_libvirt_volumes do
  $virt = Libvirt::open("qemu:///system")
  begin
    pool = $virt.lookup_storage_pool_by_name('default')
  rescue Libvirt::RetrieveError
    # Expected if the pool does not exist
  else
    for disk in pool.list_volumes do
      if /^tails-builder-/.match(disk)
        begin
          pool.lookup_volume_by_name(disk).delete
        rescue Libvirt::RetrieveError
          # Expected if the disk does not exist
        end
      end
    end
  ensure
    $virt.close
  end
end

532
533
534
def on_jenkins?
  !!ENV['JENKINS_URL']
end
535

536
537
538
539
540
541
542
543
544
desc 'Test Tails'
task :test do
  args = ARGV.drop_while { |x| x == 'test' || x == '--' }
  if on_jenkins?
    args += ['--'] unless args.include? '--'
    if not(is_release?)
      args += ['--tag', '~@fragile']
    end
    base_branch = git_helper('base_branch')
545
    if git_helper('git_only_doc_changes_since?', "origin/#{base_branch}") then
546
547
548
549
550
      args += ['--tag', '@doc']
    end
  end
  run_command('./run_test_suite', *args)
end
551

anonym's avatar
anonym committed
552
553
desc 'Clean up all build related files'
task :clean_all => ['vm:destroy', 'basebox:clean_all']
554
555
556

namespace :vm do
  desc 'Start the build virtual machine'
557
  task :up => ['parse_build_options', 'validate_http_proxy', 'setup_environment', 'basebox:create'] do
anonym's avatar
anonym committed
558
    case vm_state
559
    when :not_created
560
      clean_up_builder_vms
561
    end
562
563
564
565
566
    begin
      run_vagrant('up')
    rescue VagrantCommandError => e
      clean_up_builder_vms if $force_cleanup
      raise e
567
568
569
    end
  end

570
571
572
573
574
  desc 'SSH into the builder VM'
  task :ssh do
    run_vagrant('ssh')
  end

575
576
  desc 'Stop the build virtual machine'
  task :halt do
577
    run_vagrant('halt')
578
579
580
  end

  desc 'Re-run virtual machine setup'
anonym's avatar
anonym committed
581
  task :provision => ['parse_build_options', 'validate_http_proxy', 'setup_environment'] do
582
    run_vagrant('provision')
583
584
  end

585
  desc "Destroy build virtual machine (clean up all files except the vmproxy's apt-cacher-ng data)"
586
  task :destroy do
587
    clean_up_builder_vms
588
589
  end
end
590
591
592

namespace :basebox do

593
  desc 'Create and import the base box unless already done'
594
  task :create do
595
    next if has_box?
596
597
    $stderr.puts <<-END_OF_MESSAGE.gsub(/^      /, '')

598
599
600
601
      This is the first time we are using this Vagrant base box so we
      will have to bootstrap by building it from scratch. This will
      take around 20 minutes (depending on your hardware) plus the
      time needed for downloading around 250 MiB of Debian packages.
602
603

    END_OF_MESSAGE
604
    box_dir = VAGRANT_PATH + '/definitions/tails-builder'
605
    run_command("#{box_dir}/generate-tails-builder-box.sh")
606
607
    # Let's use an absolute path since run_vagrant changes the working
    # directory but File.delete doesn't
608
    box_path = "#{box_dir}/#{box_name}.box"
609
610
611
    run_vagrant('box', 'add', '--name', box_name, box_path)
    File.delete(box_path)
    end
612

613
614
615
  def basebox_date(box)
    Date.parse(/^tails-builder-[^-]+-[^-]+-(\d{8})/.match(box)[1])
  end
616

617
618
619
620
621
  def baseboxes
    capture_vagrant('box', 'list').first.lines
      .grep(/^tails-builder-.*/)
      .map { |x| x.chomp.sub(/\s.*$/, '') }
  end
622

623
624
625
626
627
628
629
630
631
632
633
634
635
636
  def clean_up_basebox(box)
    run_vagrant('box', 'remove', '--force', box)
    begin
      $virt = Libvirt::open("qemu:///system")
      $virt
        .lookup_storage_pool_by_name('default')
        .lookup_volume_by_name("#{box}_vagrant_box_image_0.img")
        .delete
    rescue Libvirt::RetrieveError
      # Expected if the pool or disk does not exist
    ensure
      $virt.close
    end
  end
637

638
639
  desc 'Remove all base boxes'
  task :clean_all do
640
    baseboxes.each { |box| clean_up_basebox(box) }
641
642
  end

643
644
  desc 'Remove all base boxes older than six months'
  task :clean_old do
645
    boxes = baseboxes
646
    # We always want to keep the newest basebox
647
    boxes.sort! { |a, b| basebox_date(a) <=> basebox_date(b) }
648
649
    boxes.pop
    boxes.each do |box|
650
      if basebox_date(box) < Date.today - 365.0/2.0
651
        clean_up_basebox(box)
652
653
654
      end
    end
  end
655
end