diagnostic.rb 33.5 KB
Newer Older
1
# typed: false
2
3
# frozen_string_literal: true

4
5
6
require "keg"
require "language/python"
require "formula"
hyuraku's avatar
hyuraku committed
7
require "formulary"
8
require "version"
9
require "development_tools"
10
require "utils/shell"
11
12
13
require "system_config"
require "cask/caskroom"
require "cask/quarantine"
14
15

module Homebrew
16
17
18
  # Module containing diagnostic checks.
  #
  # @api private
19
  module Diagnostic
20
    def self.missing_deps(ff, hide = nil)
21
22
      missing = {}
      ff.each do |f|
23
        missing_dependencies = f.missing_dependencies(hide: hide)
Mike McQuaid's avatar
Mike McQuaid committed
24
        next if missing_dependencies.empty?
Markus Reiter's avatar
Markus Reiter committed
25

Mike McQuaid's avatar
Mike McQuaid committed
26
27
        yield f.full_name, missing_dependencies if block_given?
        missing[f.full_name] = missing_dependencies
28
29
30
31
      end
      missing
    end

32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    def self.checks(type, fatal: true)
      @checks ||= Checks.new
      failed = false
      @checks.public_send(type).each do |check|
        out = @checks.public_send(check)
        next if out.nil?

        if fatal
          failed ||= true
          ofail out
        else
          opoo out
        end
      end
      exit 1 if failed && fatal
    end

49
    # Diagnostic checks.
50
    class Checks
Markus Reiter's avatar
Markus Reiter committed
51
52
      extend T::Sig

Mike McQuaid's avatar
Mike McQuaid committed
53
      def initialize(verbose: true)
54
55
56
        @verbose = verbose
      end

EricFromCanada's avatar
EricFromCanada committed
57
      ############# @!group HELPERS
58
      # Finds files in `HOMEBREW_PREFIX` *and* /usr/local.
EricFromCanada's avatar
EricFromCanada committed
59
      # Specify paths relative to a prefix, e.g. "include/foo.h".
60
61
      # Sets @found for your convenience.
      def find_relative_paths(*relative_paths)
Mike McQuaid's avatar
Mike McQuaid committed
62
        @found = [HOMEBREW_PREFIX, "/usr/local"].uniq.reduce([]) do |found, prefix|
63
64
65
66
          found + relative_paths.map { |f| File.join(prefix, f) }.select { |f| File.exist? f }
        end
      end

67
      def inject_file_list(list, string)
68
69
        list.reduce(string.dup) { |acc, elem| acc << "  #{elem}\n" }
            .freeze
70
      end
71
72
73
74
75

      def user_tilde(path)
        path.gsub(ENV["HOME"], "~")
      end

Markus Reiter's avatar
Markus Reiter committed
76
      sig { returns(String) }
77
78
79
80
81
82
83
      def none_string
        "<NONE>"
      end

      def add_info(*args)
        ohai(*args) if @verbose
      end
EricFromCanada's avatar
EricFromCanada committed
84
      ############# @!endgroup END HELPERS
85

Mike McQuaid's avatar
Mike McQuaid committed
86
      def fatal_preinstall_checks
87
88
89
90
91
        %w[
          check_access_directories
        ].freeze
      end

Mike McQuaid's avatar
Mike McQuaid committed
92
      def fatal_build_from_source_checks
93
94
        %w[
          check_for_installed_developer_tools
95
        ].freeze
96
97
      end

98
99
100
101
      def fatal_setup_build_environment_checks
        [].freeze
      end

Mike McQuaid's avatar
Mike McQuaid committed
102
103
104
105
106
107
      def supported_configuration_checks
        [].freeze
      end

      def build_from_source_checks
        [].freeze
108
      end
109
110

      def build_error_checks
Mike McQuaid's avatar
Mike McQuaid committed
111
        supported_configuration_checks + build_from_source_checks
112
113
      end

114
115
      def please_create_pull_requests(what = "unsupported configuration")
        <<~EOS
116
117
          You will encounter build failures with some formulae.
          Please create pull requests instead of asking for help on Homebrew's GitHub,
118
119
          Twitter or any other official channels. You are responsible for resolving
          any issues you experience while you are running this
Mike McQuaid's avatar
Mike McQuaid committed
120
          #{what}.
121
122
123
        EOS
      end

124
      def examine_git_origin(repository_path, desired_origin)
Markus Reiter's avatar
Markus Reiter committed
125
        return if !Utils::Git.available? || !repository_path.git?
126
127

        current_origin = repository_path.git_origin
hyuraku's avatar
hyuraku committed
128

129
130
131
132
133
134
        if current_origin.nil?
          <<~EOS
            Missing #{desired_origin} git origin remote.

            Without a correctly configured origin, Homebrew won't update
            properly. You can solve this by adding the remote:
135
              git -C "#{repository_path}" remote add origin #{Formatter.url(desired_origin)}
136
          EOS
hyuraku's avatar
hyuraku committed
137
        elsif !current_origin.match?(%r{#{desired_origin}(\.git|/)?$}i)
138
139
140
141
142
143
144
          <<~EOS
            Suspicious #{desired_origin} git origin remote found.
            The current git origin is:
              #{current_origin}

            With a non-standard origin, Homebrew won't update properly.
            You can solve this by setting the origin remote:
145
              git -C "#{repository_path}" remote set-url origin #{Formatter.url(desired_origin)}
146
147
148
149
          EOS
        end
      end

150
151
152
      def check_for_installed_developer_tools
        return if DevelopmentTools.installed?

Markus Reiter's avatar
Markus Reiter committed
153
        <<~EOS
154
          No developer tools installed.
155
          #{DevelopmentTools.installation_instructions}
156
157
158
        EOS
      end

159
160
161
162
163
164
165
166
      # Anaconda installs multiple system & brew dupes, including OpenSSL, Python,
      # sqlite, libpng, Qt, etc. Regularly breaks compile on Vim, MacVim and others.
      # Is flagged as part of the *-config script checks below, but people seem
      # to ignore those as warnings rather than extremely likely breakage.
      def check_for_anaconda
        return unless which("anaconda")
        return unless which("python")

167
        anaconda_directory = which("anaconda").realpath.dirname
168
        python_binary = Utils.popen_read(which("python"), "-c", "import sys; sys.stdout.write(sys.executable)")
169
        python_directory = Pathname.new(python_binary).realpath.dirname
170
171

        # Only warn if Python lives with Anaconda, since is most problematic case.
172
173
        return unless python_directory == anaconda_directory

Markus Reiter's avatar
Markus Reiter committed
174
        <<~EOS
175
          Anaconda is known to frequently break Homebrew builds, including Vim and
EricFromCanada's avatar
EricFromCanada committed
176
          MacVim, due to bundling many duplicates of system and Homebrew-provided
177
178
179
180
181
182
183
184
          tools.

          If you encounter a build failure please temporarily remove Anaconda
          from your $PATH and attempt the build again prior to reporting the
          failure to us. Thanks!
        EOS
      end

185
      def __check_stray_files(dir, pattern, allow_list, message)
186
187
188
        return unless File.directory?(dir)

        files = Dir.chdir(dir) do
189
          (Dir.glob(pattern) - Dir.glob(allow_list))
190
191
192
            .select { |f| File.file?(f) && !File.symlink?(f) }
            .map { |f| File.join(dir, f) }
        end
193
        return if files.empty?
194

195
        inject_file_list(files.sort, message)
196
197
198
199
200
      end

      def check_for_stray_dylibs
        # Dylibs which are generally OK should be added to this list,
        # with a short description of the software they come with.
201
        allow_list = [
202
203
204
205
206
207
          "libfuse.2.dylib", # MacFuse
          "libfuse_ino64.2.dylib", # MacFuse
          "libmacfuse_i32.2.dylib", # OSXFuse MacFuse compatibility layer
          "libmacfuse_i64.2.dylib", # OSXFuse MacFuse compatibility layer
          "libosxfuse_i32.2.dylib", # OSXFuse
          "libosxfuse_i64.2.dylib", # OSXFuse
208
          "libosxfuse.2.dylib", # OSXFuse
209
          "libTrAPI.dylib", # TrAPI/Endpoint Security VPN
210
211
212
213
214
          "libntfs-3g.*.dylib", # NTFS-3G
          "libntfs.*.dylib", # NTFS-3G
          "libublio.*.dylib", # NTFS-3G
          "libUFSDNTFS.dylib", # Paragon NTFS
          "libUFSDExtFS.dylib", # Paragon ExtFS
215
          "libecomlodr.dylib", # Symantec Endpoint Protection
216
          "libsymsea*.dylib", # Symantec Endpoint Protection
217
          "sentinel.dylib", # SentinelOne
218
          "sentinel-*.dylib", # SentinelOne
219
220
        ]

221
        __check_stray_files "/usr/local/lib", "*.dylib", allow_list, <<~EOS
222
223
224
          Unbrewed dylibs were found in /usr/local/lib.
          If you didn't put them there on purpose they could cause problems when
          building Homebrew formulae, and may need to be deleted.
225

226
227
          Unexpected dylibs:
        EOS
228
229
230
231
232
      end

      def check_for_stray_static_libs
        # Static libs which are generally OK should be added to this list,
        # with a short description of the software they come with.
233
        allow_list = [
234
235
236
          "libntfs-3g.a", # NTFS-3G
          "libntfs.a", # NTFS-3G
          "libublio.a", # NTFS-3G
237
238
239
240
241
242
243
          "libappfirewall.a", # Symantec Endpoint Protection
          "libautoblock.a", # Symantec Endpoint Protection
          "libautosetup.a", # Symantec Endpoint Protection
          "libconnectionsclient.a", # Symantec Endpoint Protection
          "liblocationawareness.a", # Symantec Endpoint Protection
          "libpersonalfirewall.a", # Symantec Endpoint Protection
          "libtrustedcomponents.a", # Symantec Endpoint Protection
244
245
        ]

246
        __check_stray_files "/usr/local/lib", "*.a", allow_list, <<~EOS
247
248
249
          Unbrewed static libraries were found in /usr/local/lib.
          If you didn't put them there on purpose they could cause problems when
          building Homebrew formulae, and may need to be deleted.
250

251
252
          Unexpected static libraries:
        EOS
253
254
255
256
257
      end

      def check_for_stray_pcs
        # Package-config files which are generally OK should be added to this list,
        # with a short description of the software they come with.
258
        allow_list = [
259
260
261
262
263
264
265
          "fuse.pc", # OSXFuse/MacFuse
          "macfuse.pc", # OSXFuse MacFuse compatibility layer
          "osxfuse.pc", # OSXFuse
          "libntfs-3g.pc", # NTFS-3G
          "libublio.pc", # NTFS-3G
        ]

266
        __check_stray_files "/usr/local/lib/pkgconfig", "*.pc", allow_list, <<~EOS
267
268
269
          Unbrewed .pc files were found in /usr/local/lib/pkgconfig.
          If you didn't put them there on purpose they could cause problems when
          building Homebrew formulae, and may need to be deleted.
270

271
272
          Unexpected .pc files:
        EOS
273
274
275
      end

      def check_for_stray_las
276
        allow_list = [
277
278
279
280
          "libfuse.la", # MacFuse
          "libfuse_ino64.la", # MacFuse
          "libosxfuse_i32.la", # OSXFuse
          "libosxfuse_i64.la", # OSXFuse
281
          "libosxfuse.la", # OSXFuse
282
283
284
285
286
          "libntfs-3g.la", # NTFS-3G
          "libntfs.la", # NTFS-3G
          "libublio.la", # NTFS-3G
        ]

287
        __check_stray_files "/usr/local/lib", "*.la", allow_list, <<~EOS
288
289
290
          Unbrewed .la files were found in /usr/local/lib.
          If you didn't put them there on purpose they could cause problems when
          building Homebrew formulae, and may need to be deleted.
291

292
293
          Unexpected .la files:
        EOS
294
295
296
      end

      def check_for_stray_headers
297
        allow_list = [
298
299
300
301
302
303
304
305
          "fuse.h", # MacFuse
          "fuse/**/*.h", # MacFuse
          "macfuse/**/*.h", # OSXFuse MacFuse compatibility layer
          "osxfuse/**/*.h", # OSXFuse
          "ntfs/**/*.h", # NTFS-3G
          "ntfs-3g/**/*.h", # NTFS-3G
        ]

306
        __check_stray_files "/usr/local/include", "**/*.h", allow_list, <<~EOS
307
308
309
          Unbrewed header files were found in /usr/local/include.
          If you didn't put them there on purpose they could cause problems when
          building Homebrew formulae, and may need to be deleted.
310

311
312
          Unexpected header files:
        EOS
313
314
315
316
317
      end

      def check_for_broken_symlinks
        broken_symlinks = []

318
        Keg::MUST_EXIST_SUBDIRECTORIES.each do |d|
319
          next unless d.directory?
Markus Reiter's avatar
Markus Reiter committed
320

321
          d.find do |path|
322
            broken_symlinks << path if path.symlink? && !path.resolved_path_exists?
323
324
325
          end
        end
        return if broken_symlinks.empty?
326

Markus Reiter's avatar
Markus Reiter committed
327
        inject_file_list broken_symlinks, <<~EOS
328
          Broken symlinks were found. Remove them with `brew cleanup`:
329
330
331
        EOS
      end

332
333
334
      def check_tmpdir_sticky_bit
        world_writable = HOMEBREW_TEMP.stat.mode & 0777 == 0777
        return if !world_writable || HOMEBREW_TEMP.sticky?
335

Markus Reiter's avatar
Markus Reiter committed
336
        <<~EOS
337
338
          #{HOMEBREW_TEMP} is world-writable but does not have the sticky bit set.
          Please execute `sudo chmod +t #{HOMEBREW_TEMP}` in your Terminal.
339
340
        EOS
      end
Markus Reiter's avatar
Markus Reiter committed
341
      alias generic_check_tmpdir_sticky_bit check_tmpdir_sticky_bit
342

343
344
345
      def check_exist_directories
        not_exist_dirs = Keg::MUST_EXIST_DIRECTORIES.reject(&:exist?)
        return if not_exist_dirs.empty?
346

Markus Reiter's avatar
Markus Reiter committed
347
        <<~EOS
348
349
          The following directories do not exist:
          #{not_exist_dirs.join("\n")}
350

351
352
353
          You should create these directories and change their ownership to your account.
            sudo mkdir -p #{not_exist_dirs.join(" ")}
            sudo chown -R $(whoami) #{not_exist_dirs.join(" ")}
354
355
356
        EOS
      end

357
358
359
360
      def check_access_directories
        not_writable_dirs =
          Keg::MUST_BE_WRITABLE_DIRECTORIES.select(&:exist?)
                                           .reject(&:writable_real?)
361
362
        return if not_writable_dirs.empty?

Markus Reiter's avatar
Markus Reiter committed
363
        <<~EOS
364
          The following directories are not writable by your user:
365
366
          #{not_writable_dirs.join("\n")}

367
          You should change the ownership of these directories to your user.
368
            sudo chown -R $(whoami) #{not_writable_dirs.join(" ")}
369
370
371

          And make sure that your user has write permission.
            chmod u+w #{not_writable_dirs.join(" ")}
372
373
374
        EOS
      end

375
376
377
378
379
      def check_multiple_cellars
        return if HOMEBREW_PREFIX.to_s == HOMEBREW_REPOSITORY.to_s
        return unless (HOMEBREW_REPOSITORY/"Cellar").exist?
        return unless (HOMEBREW_PREFIX/"Cellar").exist?

Markus Reiter's avatar
Markus Reiter committed
380
        <<~EOS
381
382
383
384
385
386
          You have multiple Cellars.
          You should delete #{HOMEBREW_REPOSITORY}/Cellar:
            rm -rf #{HOMEBREW_REPOSITORY}/Cellar
        EOS
      end

387
      def check_user_path_1
388
389
        @seen_prefix_bin = false
        @seen_prefix_sbin = false
390

391
        message = ""
392

Mike McQuaid's avatar
Mike McQuaid committed
393
        paths.each do |p|
394
395
          case p
          when "/usr/bin"
396
            unless @seen_prefix_bin
397
398
              # only show the doctor message if there are any conflicts
              # rationale: a default install should not trigger any brew doctor messages
399
400
401
              conflicts = Dir["#{HOMEBREW_PREFIX}/bin/*"]
                          .map { |fn| File.basename fn }
                          .select { |bn| File.exist? "/usr/bin/#{bn}" }
402

403
              unless conflicts.empty?
Markus Reiter's avatar
Markus Reiter committed
404
                message = inject_file_list conflicts, <<~EOS
405
406
                  /usr/bin occurs before #{HOMEBREW_PREFIX}/bin
                  This means that system-provided programs will be used instead of those
407
408
                  provided by Homebrew. Consider setting your PATH so that
                  #{HOMEBREW_PREFIX}/bin occurs before /usr/bin. Here is a one-liner:
Mike McQuaid's avatar
Mike McQuaid committed
409
                    #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/bin")}
410
411

                  The following tools exist at both paths:
412
                EOS
413
414
415
              end
            end
          when "#{HOMEBREW_PREFIX}/bin"
416
            @seen_prefix_bin = true
417
          when "#{HOMEBREW_PREFIX}/sbin"
418
            @seen_prefix_sbin = true
419
420
          end
        end
421
422

        message unless message.empty?
423
424
425
      end

      def check_user_path_2
426
        return if @seen_prefix_bin
427

Markus Reiter's avatar
Markus Reiter committed
428
        <<~EOS
429
          Homebrew's bin was not found in your PATH.
EricFromCanada's avatar
EricFromCanada committed
430
          Consider setting the PATH for example like so:
Mike McQuaid's avatar
Mike McQuaid committed
431
            #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/bin")}
432
433
434
435
        EOS
      end

      def check_user_path_3
436
        return if @seen_prefix_sbin
437

438
        # Don't complain about sbin not being in the path if it doesn't exist
439
        sbin = HOMEBREW_PREFIX/"sbin"
440
441
442
        return unless sbin.directory?
        return if sbin.children.empty?
        return if sbin.children.one? && sbin.children.first.basename.to_s == ".keepme"
443

Markus Reiter's avatar
Markus Reiter committed
444
        <<~EOS
445
446
          Homebrew's sbin was not found in your PATH but you have installed
          formulae that put executables in #{HOMEBREW_PREFIX}/sbin.
EricFromCanada's avatar
EricFromCanada committed
447
          Consider setting the PATH for example like so:
Mike McQuaid's avatar
Mike McQuaid committed
448
            #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/sbin")}
449
        EOS
450
451
452
453
      end

      def check_for_config_scripts
        return unless HOMEBREW_CELLAR.exist?
Markus Reiter's avatar
Markus Reiter committed
454

455
456
457
458
        real_cellar = HOMEBREW_CELLAR.realpath

        scripts = []

459
        allowlist = %W[
460
          /bin /sbin
461
462
463
464
465
466
467
          /usr/bin /usr/sbin
          /usr/X11/bin /usr/X11R6/bin /opt/X11/bin
          #{HOMEBREW_PREFIX}/bin #{HOMEBREW_PREFIX}/sbin
          /Applications/Server.app/Contents/ServerRoot/usr/bin
          /Applications/Server.app/Contents/ServerRoot/usr/sbin
        ].map(&:downcase)

Mike McQuaid's avatar
Mike McQuaid committed
468
        paths.each do |p|
469
          next if allowlist.include?(p.downcase) || !File.directory?(p)
470
471
472
473
474
475
476
477

          realpath = Pathname.new(p).realpath.to_s
          next if realpath.start_with?(real_cellar.to_s, HOMEBREW_CELLAR.to_s)

          scripts += Dir.chdir(p) { Dir["*-config"] }.map { |c| File.join(p, c) }
        end

        return if scripts.empty?
478

Markus Reiter's avatar
Markus Reiter committed
479
        inject_file_list scripts, <<~EOS
480
481
          "config" scripts exist outside your system or Homebrew directories.
          `./configure` scripts often look for *-config scripts to determine if
EricFromCanada's avatar
EricFromCanada committed
482
          software packages are installed, and which additional flags to use when
483
484
485
          compiling and linking.

          Having additional scripts in your path can confuse software installed via
EricFromCanada's avatar
EricFromCanada committed
486
          Homebrew if the config script overrides a system or Homebrew-provided
487
488
489
490
491
492
          script of the same name. We found the following "config" scripts:
        EOS
      end

      def check_for_symlinked_cellar
        return unless HOMEBREW_CELLAR.exist?
493
494
        return unless HOMEBREW_CELLAR.symlink?

Markus Reiter's avatar
Markus Reiter committed
495
        <<~EOS
496
497
498
499
500
501
502
503
504
          Symlinked Cellars can cause problems.
          Your Homebrew Cellar is a symlink: #{HOMEBREW_CELLAR}
                          which resolves to: #{HOMEBREW_CELLAR.realpath}

          The recommended Homebrew installations are either:
          (A) Have Cellar be a real directory inside of your HOMEBREW_PREFIX
          (B) Symlink "bin/brew" into your prefix, but don't symlink "Cellar".

          Older installations of Homebrew may have created a symlinked Cellar, but this can
EricFromCanada's avatar
EricFromCanada committed
505
          cause problems when two formulae install to locations that are mapped on top of each
506
507
508
509
          other during the linking step.
        EOS
      end

510
      def check_git_version
Mike McQuaid's avatar
Mike McQuaid committed
511
        minimum_version = ENV["HOMEBREW_MINIMUM_GIT_VERSION"]
Markus Reiter's avatar
Markus Reiter committed
512
513
        return unless Utils::Git.available?
        return if Version.create(Utils::Git.version) >= Version.create(minimum_version)
514

515
516
        git = Formula["git"]
        git_upgrade_cmd = git.any_version_installed? ? "upgrade" : "install"
Markus Reiter's avatar
Markus Reiter committed
517
        <<~EOS
Markus Reiter's avatar
Markus Reiter committed
518
          An outdated version (#{Utils::Git.version}) of Git was detected in your PATH.
Mike McQuaid's avatar
Mike McQuaid committed
519
          Git #{minimum_version} or newer is required for Homebrew.
520
521
522
          Please upgrade:
            brew #{git_upgrade_cmd} git
        EOS
523
524
525
      end

      def check_for_git
Markus Reiter's avatar
Markus Reiter committed
526
        return if Utils::Git.available?
527

Markus Reiter's avatar
Markus Reiter committed
528
        <<~EOS
529
530
531
532
533
          Git could not be found in your PATH.
          Homebrew uses Git for several internal functions, and some formulae use Git
          checkouts instead of stable tarballs. You may want to install Git:
            brew install git
        EOS
534
535
536
      end

      def check_git_newline_settings
Markus Reiter's avatar
Markus Reiter committed
537
        return unless Utils::Git.available?
538

539
        autocrlf = HOMEBREW_REPOSITORY.cd { `git config --get core.autocrlf`.chomp }
540
        return unless autocrlf == "true"
541

Markus Reiter's avatar
Markus Reiter committed
542
        <<~EOS
543
          Suspicious Git newline settings found.
544

545
546
          The detected Git newline settings will cause checkout problems:
            core.autocrlf = #{autocrlf}
547

548
549
550
          If you are not routinely dealing with Windows-based projects,
          consider removing these by running:
            git config --global core.autocrlf input
551
552
553
        EOS
      end

554
      def check_brew_git_origin
Mike McQuaid's avatar
Mike McQuaid committed
555
        examine_git_origin(HOMEBREW_REPOSITORY, Homebrew::EnvConfig.brew_git_remote)
556
557
      end

558
      def check_coretap_git_origin
Mike McQuaid's avatar
Mike McQuaid committed
559
        examine_git_origin(CoreTap.instance.path, Homebrew::EnvConfig.core_git_remote)
560
      end
561

562
      def check_casktap_git_origin
563
564
565
566
        cask_tap = Tap.default_cask_tap
        return unless cask_tap.installed?

        examine_git_origin(cask_tap.path, cask_tap.remote)
Shaun Jackman's avatar
Shaun Jackman committed
567
      end
Ben Muschol's avatar
Ben Muschol committed
568

569
570
      sig { returns(T.nilable(String)) }
      def check_tap_git_branch
571
        return if ENV["CI"]
572
        return unless Utils::Git.available?
Ben Muschol's avatar
Ben Muschol committed
573

574
575
576
        commands = Tap.map do |tap|
          next unless tap.path.git?
          next if tap.path.git_origin.blank?
Shaun Jackman's avatar
Shaun Jackman committed
577

578
579
          branch = tap.path.git_branch
          next if branch.blank?
Ben Muschol's avatar
Ben Muschol committed
580

581
582
583
584
585
          origin_branch = Utils::Git.origin_branch(tap.path)&.split("/")&.last
          next if origin_branch == branch

          "git -C $(brew --repo #{tap.name}) checkout #{origin_branch}"
        end.compact
Ben Muschol's avatar
Ben Muschol committed
586

587
588
589
590
591
592
        return if commands.blank?

        <<~EOS
          Some taps are not on the default git origin branch and may not receive
          updates. If this is a surprise to you, check out the default branch with:
            #{commands.join("\n  ")}
Ben Muschol's avatar
Ben Muschol committed
593
        EOS
594
595
      end

596
597
598
599
600
601
602
      def check_deprecated_official_taps
        tapped_deprecated_taps =
          Tap.select(&:official?).map(&:repo) & DEPRECATED_OFFICIAL_TAPS
        return if tapped_deprecated_taps.empty?

        <<~EOS
          You have the following deprecated, official taps tapped:
603
            Homebrew/homebrew-#{tapped_deprecated_taps.join("\n  Homebrew/homebrew-")}
604
605
606
607
          Untap them with `brew untap`.
        EOS
      end

608
609
610
611
      def __check_linked_brew(f)
        f.installed_prefixes.each do |prefix|
          prefix.find do |src|
            next if src == prefix
Markus Reiter's avatar
Markus Reiter committed
612

613
614
615
616
617
618
619
620
621
622
            dst = HOMEBREW_PREFIX + src.relative_path_from(prefix)
            return true if dst.symlink? && src == dst.resolved_path
          end
        end

        false
      end

      def check_for_other_frameworks
        # Other frameworks that are known to cause problems when present
623
624
625
626
627
        frameworks_to_check = %w[
          expat.framework
          libexpat.framework
          libcurl.framework
        ]
628
629
630
        frameworks_found = frameworks_to_check
                           .map { |framework| "/Library/Frameworks/#{framework}" }
                           .select { |framework| File.exist? framework }
631
632
        return if frameworks_found.empty?

Markus Reiter's avatar
Markus Reiter committed
633
        inject_file_list frameworks_found, <<~EOS
EricFromCanada's avatar
EricFromCanada committed
634
          Some frameworks can be picked up by CMake's build system and will likely
635
636
637
          cause the build to fail. To compile CMake, you may wish to move these
          out of the way:
        EOS
638
639
640
641
      end

      def check_tmpdir
        tmpdir = ENV["TMPDIR"]
642
643
        return if tmpdir.nil? || File.directory?(tmpdir)

Markus Reiter's avatar
Markus Reiter committed
644
        <<~EOS
645
646
          TMPDIR #{tmpdir.inspect} doesn't exist.
        EOS
647
648
649
650
      end

      def check_missing_deps
        return unless HOMEBREW_CELLAR.exist?
Markus Reiter's avatar
Markus Reiter committed
651

652
653
654
655
        missing = Set.new
        Homebrew::Diagnostic.missing_deps(Formula.installed).each_value do |deps|
          missing.merge(deps)
        end
656
        return if missing.empty?
657

Markus Reiter's avatar
Markus Reiter committed
658
        <<~EOS
659
          Some installed formulae are missing dependencies.
660
          You should `brew install` the missing dependencies:
661
            brew install #{missing.sort_by(&:full_name) * " "}
662
663
664
665
666

          Run `brew missing` for more details.
        EOS
      end

667
668
669
670
671
672
673
674
675
676
677
678
679
680
      def check_deprecated_disabled
        return unless HOMEBREW_CELLAR.exist?

        deprecated_or_disabled = Formula.installed.select(&:deprecated?)
        deprecated_or_disabled += Formula.installed.select(&:disabled?)
        return if deprecated_or_disabled.empty?

        <<~EOS
          Some installed formulae are deprecated or disabled.
          You should find replacements for the following formulae:
            #{deprecated_or_disabled.sort_by(&:full_name).uniq * "\n  "}
        EOS
      end

681
      def check_git_status
Markus Reiter's avatar
Markus Reiter committed
682
        return unless Utils::Git.available?
Markus Reiter's avatar
Markus Reiter committed
683

684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
        message = nil

        {
          "Homebrew/brew"          => HOMEBREW_REPOSITORY,
          "Homebrew/homebrew-core" => CoreTap.instance.path,
        }.each do |name, path|
          status = path.cd do
            `git status --untracked-files=all --porcelain 2>/dev/null`
          end
          next if status.blank?

          message ||= ""
          message += "\n" unless message.empty?
          message += <<~EOS
            You have uncommitted modifications to #{name}.
            If this is a surprise to you, then you should stash these modifications.
            Stashing returns Homebrew to a pristine state but can be undone
            should you later need to do so for some reason.
              cd #{path} && git stash && git clean -d -f
          EOS
704
705

          modified = status.split("\n")
706
707
          message += inject_file_list modified, <<~EOS

708
            Uncommitted files:
709
710
711
712
          EOS
        end

        message
713
714
715
716
      end

      def check_for_bad_python_symlink
        return unless which "python"
Markus Reiter's avatar
Markus Reiter committed
717

718
719
        `python -V 2>&1` =~ /Python (\d+)\./
        # This won't be the right warning if we matched nothing at all
720
721
        return if Regexp.last_match(1).nil?
        return if Regexp.last_match(1) == "2"
722

Markus Reiter's avatar
Markus Reiter committed
723
        <<~EOS
724
          python is symlinked to python#{Regexp.last_match(1)}
725
          This will confuse build scripts and in general lead to subtle breakage.
726
727
728
729
        EOS
      end

      def check_for_non_prefixed_coreutils
730
731
732
733
734
735
        coreutils = Formula["coreutils"]
        return unless coreutils.any_version_installed?

        gnubin = %W[#{coreutils.opt_libexec}/gnubin #{coreutils.libexec}/gnubin]
        return if (paths & gnubin).empty?

Markus Reiter's avatar
Markus Reiter committed
736
        <<~EOS
737
          Putting non-prefixed coreutils in your path can cause gmp builds to fail.
738
        EOS
739
      rescue FormulaUnavailableError
Mike McQuaid's avatar
Mike McQuaid committed
740
        nil
741
742
743
      end

      def check_for_pydistutils_cfg_in_home
744
745
        return unless File.exist? "#{ENV["HOME"]}/.pydistutils.cfg"

Markus Reiter's avatar
Markus Reiter committed
746
        <<~EOS
747
748
          A .pydistutils.cfg file was found in $HOME, which may cause Python
          builds to fail. See:
Markus Reiter's avatar
Markus Reiter committed
749
750
            #{Formatter.url("https://bugs.python.org/issue6138")}
            #{Formatter.url("https://bugs.python.org/issue4655")}
751
752
753
        EOS
      end

754
755
756
      def check_for_unreadable_installed_formula
        formula_unavailable_exceptions = []
        Formula.racks.each do |rack|
757
758
759
760
761
762
763
          Formulary.from_rack(rack)
        rescue FormulaUnreadableError, FormulaClassUnavailableError,
               TapFormulaUnreadableError, TapFormulaClassUnavailableError => e
          formula_unavailable_exceptions << e
        rescue FormulaUnavailableError,
               TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
          nil
764
765
766
767
768
769
770
771
772
        end
        return if formula_unavailable_exceptions.empty?

        <<~EOS
          Some installed formulae are not readable:
            #{formula_unavailable_exceptions.join("\n\n  ")}
        EOS
      end

773
774
      def check_for_unlinked_but_not_keg_only
        unlinked = Formula.racks.reject do |rack|
775
776
777
          if (HOMEBREW_LINKED_KEGS/rack.basename).directory?
            true
          else
778
779
780
781
782
783
784
785
            begin
              Formulary.from_rack(rack).keg_only?
            rescue FormulaUnavailableError, TapFormulaAmbiguityError, TapFormulaWithOldnameAmbiguityError
              false
            end
          end
        end.map(&:basename)
        return if unlinked.empty?
786

Markus Reiter's avatar
Markus Reiter committed
787
        inject_file_list unlinked, <<~EOS
EricFromCanada's avatar
EricFromCanada committed
788
          You have unlinked kegs in your Cellar.
789
790
          Leaving kegs unlinked can lead to build-trouble and cause brews that depend on
          those kegs to fail to run properly once built. Run `brew link` on these:
791
792
793
794
        EOS
      end

      def check_for_external_cmd_name_conflict
Mike McQuaid's avatar
Mike McQuaid committed
795
        cmds = Tap.cmd_directories.flat_map { |p| Dir["#{p}/brew-*"] }.uniq
796
797
798
799
800
801
802
803
804
        cmds = cmds.select { |cmd| File.file?(cmd) && File.executable?(cmd) }
        cmd_map = {}
        cmds.each do |cmd|
          cmd_name = File.basename(cmd, ".rb")
          cmd_map[cmd_name] ||= []
          cmd_map[cmd_name] << cmd
        end
        cmd_map.reject! { |_cmd_name, cmd_paths| cmd_paths.size == 1 }
        return if cmd_map.empty?
805

806
807
808
809
810
        if ENV["CI"] && cmd_map.keys.length == 1 &&
           cmd_map.keys.first == "brew-test-bot"
          return
        end

811
        message = "You have external commands with conflicting names.\n"
812
        cmd_map.each do |cmd_name, cmd_paths|
Markus Reiter's avatar
Markus Reiter committed
813
          message += inject_file_list cmd_paths, <<~EOS
EricFromCanada's avatar
EricFromCanada committed
814
            Found command `#{cmd_name}` in the following places:
815
816
          EOS
        end
817
818

        message
819
820
      end

821
822
823
824
825
826
      def check_for_tap_ruby_files_locations
        bad_tap_files = {}
        Tap.each do |tap|
          unused_formula_dirs = tap.potential_formula_dirs - [tap.formula_dir]
          unused_formula_dirs.each do |dir|
            next unless dir.exist?
Markus Reiter's avatar
Markus Reiter committed
827

828
829
            dir.children.each do |path|
              next unless path.extname == ".rb"
Markus Reiter's avatar
Markus Reiter committed
830

831
832
833
834
835
836
              bad_tap_files[tap] ||= []
              bad_tap_files[tap] << path
            end
          end
        end
        return if bad_tap_files.empty?
Markus Reiter's avatar
Markus Reiter committed
837

838
        bad_tap_files.keys.map do |tap|
Markus Reiter's avatar
Markus Reiter committed
839
          <<~EOS
EricFromCanada's avatar
EricFromCanada committed
840
            Found Ruby file outside #{tap} tap formula directory.
841
842
843
844
845
846
            (#{tap.formula_dir}):
              #{bad_tap_files[tap].join("\n  ")}
          EOS
        end.join("\n")
      end

847
      def check_homebrew_prefix
848
        return if Homebrew.default_prefix?
849
850
851

        <<~EOS
          Your Homebrew's prefix is not #{Homebrew::DEFAULT_PREFIX}.
852
853
854
          Some of Homebrew's bottles (binary packages) can only be used with the default
          prefix (#{Homebrew::DEFAULT_PREFIX}).
          #{please_create_pull_requests}
855
856
857
        EOS
      end

hyuraku's avatar
hyuraku committed
858
      def check_deleted_formula
hyuraku's avatar
hyuraku committed
859
        kegs = Keg.all
hyuraku's avatar
hyuraku committed
860
        deleted_formulae = []
hyuraku's avatar
hyuraku committed
861
        kegs.each do |keg|
hyuraku's avatar
hyuraku committed
862
          keg_name = keg.name
hyuraku's avatar
hyuraku committed
863
          deleted_formulae << keg_name if Formulary.tap_paths(keg_name).blank?
hyuraku's avatar
hyuraku committed
864
865
866
        end
        return if deleted_formulae.blank?

Mike McQuaid's avatar
Mike McQuaid committed
867
        <<~EOS
868
869
          Some installed kegs have no formulae!
          This means they were either deleted or installed with `brew diy`.
hyuraku's avatar
hyuraku committed
870
          You should find replacements for the following formulae:
hyuraku's avatar
hyuraku committed
871
            #{deleted_formulae.join("\n  ")}
hyuraku's avatar
hyuraku committed
872
873
874
        EOS
      end

875
876
877
878
879
880
881
882
883
      def check_cask_software_versions
        add_info "Homebrew Version", HOMEBREW_VERSION
        add_info "macOS", MacOS.full_version
        add_info "SIP", begin
          csrutil = "/usr/bin/csrutil"
          if File.executable?(csrutil)
            Open3.capture2(csrutil, "status")
                 .first
                 .gsub("This is an unsupported configuration, likely to break in " \
884
                       "the future and leave your machine in an unknown state.", "")
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
                 .gsub("System Integrity Protection status: ", "")
                 .delete("\t\.")
                 .capitalize
                 .strip
          else
            "N/A"
          end
        end
        add_info "Java", SystemConfig.describe_java

        nil
      end

      def check_cask_install_location
        locations = Dir.glob(HOMEBREW_CELLAR.join("brew-cask", "*")).reverse
        return if locations.empty?

        locations.map do |l|
          "Legacy install at #{l}. Run `brew uninstall --force brew-cask`."
        end.join "\n"
      end

      def check_cask_staging_location
908
        # Skip this check when running CI since the staging path is not writable for security reasons
909
        return if ENV["GITHUB_ACTIONS"]
910

911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
        path = Cask::Caskroom.path

        add_info "Homebrew Cask Staging Location", user_tilde(path.to_s)

        return unless path.exist? && !path.writable?

        <<~EOS
          The staging path #{user_tilde(path.to_s)} is not writable by the current user.
          To fix, run \'sudo chown -R $(whoami):staff #{user_tilde(path.to_s)}'
        EOS
      end

      def check_cask_taps
        default_tap = Tap.default_cask_tap
        alt_taps = Tap.select { |t| t.cask_dir.exist? && t != default_tap }

        error_tap_paths = []

        add_info "Homebrew Cask Taps:", ([default_tap, *alt_taps].map do |tap|
          if tap.path.blank?
            none_string
          else
            cask_count = begin
Mike McQuaid's avatar
Mike McQuaid committed
934
935
936
937
938
              tap.cask_files.count
            rescue
              error_tap_paths << tap.path
              0
            end
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986

            "#{tap.path} (#{cask_count} #{"cask".pluralize(cask_count)})"
          end
        end)

        taps = "tap".pluralize error_tap_paths.count
        "Unable to read from cask #{taps}: #{error_tap_paths.to_sentence}" if error_tap_paths.present?
      end

      def check_cask_load_path
        paths = $LOAD_PATH.map(&method(:user_tilde))

        add_info "$LOAD_PATHS", paths.presence || none_string

        "$LOAD_PATH is empty" if paths.blank?
      end

      def check_cask_environment_variables
        environment_variables = %w[
          RUBYLIB
          RUBYOPT
          RUBYPATH
          RBENV_VERSION
          CHRUBY_VERSION
          GEM_HOME
          GEM_PATH
          BUNDLE_PATH
          PATH
          SHELL
          HOMEBREW_CASK_OPTS
        ]

        locale_variables = ENV.keys.grep(/^(?:LC_\S+|LANG|LANGUAGE)\Z/).sort

        add_info "Cask Environment Variables:", ((locale_variables + environment_variables).sort.each do |var|
          next unless ENV.key?(var)

          var = %Q(#{var}="#{ENV[var]}")
          user_tilde(var)
        end)
      end

      def check_cask_xattr
        result = system_command "/usr/bin/xattr"

        return if result.status.success?

        if result.stderr.include? "ImportError: No module named pkg_resources"
Jonathan Chang's avatar
Jonathan Chang committed
987
          result = Utils.popen_read "/usr/bin/python", "--version", err: :out
988

Jonathan Chang's avatar
Jonathan Chang committed
989
          if result.include? "Python 2.7"
990
991
992
993
994
995
996
997
998
999
1000
            <<~EOS
              Your Python installation has a broken version of setuptools.
              To fix, reinstall macOS or run 'sudo /usr/bin/python -m pip install -I setuptools'.
            EOS
          else
            <<~EOS
              The system Python version is wrong.
              To fix, run 'defaults write com.apple.versioner.python Version 2.7'.
            EOS
          end
        elsif result.stderr.include? "pkg_resources.DistributionNotFound"
For faster browsing, not all history is shown. View entire blame