├── sample_listfiles ├── list_test ├── list_full └── list_current ├── .gitignore ├── currentUserPerm.sh ├── README.md └── brew-stew /sample_listfiles/list_test: -------------------------------------------------------------------------------- 1 | git 2 | pyenv 3 | python 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.pkg 3 | *.txt 4 | *.log 5 | list_me 6 | -------------------------------------------------------------------------------- /sample_listfiles/list_full: -------------------------------------------------------------------------------- 1 | #android-sdk 2 | ant 3 | autoconf 4 | bison 5 | cowsay 6 | curl 7 | cvs 8 | dnsmasq 9 | docker-compose 10 | docker-machine 11 | docker-swarm 12 | elasticsearch 13 | fontconfig 14 | freetype 15 | gd 16 | gdbm 17 | gettext 18 | git 19 | git-extras 20 | gmp 21 | gnupg 22 | go 23 | gradle 24 | graphviz 25 | groovy 26 | httpie 27 | jenkins-lts 28 | jenv 29 | jid 30 | jpeg 31 | jq 32 | jsonpp 33 | kibana 34 | libevent 35 | libpng 36 | libtiff 37 | libtool 38 | libyaml 39 | logstash 40 | makedepend 41 | mercurial 42 | moreutils 43 | ngrep 44 | node 45 | nvm 46 | oniguruma 47 | openssl 48 | openssl@1.1 49 | p7zip 50 | pandoc 51 | pcre 52 | pkg-config 53 | plantuml 54 | pyenv 55 | pyenv-virtualenv 56 | pyenv-virtualenvwrapper 57 | python 58 | python3 59 | qlmarkdown 60 | rbenv 61 | rbenv-bundler 62 | readline 63 | ruby-build 64 | sbt 65 | scala 66 | squid 67 | ssh-copy-id 68 | the_silver_searcher 69 | tig 70 | tree 71 | utf8proc 72 | watch 73 | webp 74 | wget 75 | xz 76 | yourkit-java-profiler 77 | -------------------------------------------------------------------------------- /sample_listfiles/list_current: -------------------------------------------------------------------------------- 1 | ant 2 | autoconf 3 | bison 4 | carthage 5 | cntlm 6 | cocoapods 7 | coreutils 8 | curl 9 | cvs 10 | cvsutils 11 | daq 12 | dialog 13 | dirmngr 14 | dnsmasq 15 | docker-compose 16 | docker-machine 17 | docker-swarm 18 | elasticsearch 19 | fish 20 | flow 21 | fontconfig 22 | freetype 23 | gd 24 | gdbm 25 | gettext 26 | git 27 | git-extras 28 | gmp 29 | gnupg2 30 | gpg-agent 31 | go 32 | gradle 33 | graphviz 34 | groovy 35 | htop 36 | httpie 37 | icu4c 38 | jenkins-lts 39 | jenv 40 | jid 41 | jpeg 42 | jq 43 | jsonpp 44 | kibana 45 | libassuan 46 | libdnet 47 | libevent 48 | libgcrypt 49 | libksba 50 | libpng 51 | libtiff 52 | libtool 53 | libusb 54 | libusb-compat 55 | libxml2 56 | libyaml 57 | logstash 58 | luajit 59 | makedepend 60 | mercurial 61 | moreutils 62 | ncdu 63 | ngrep 64 | node 65 | nvm 66 | oniguruma 67 | openssl 68 | openssl@1.1 69 | p7zip 70 | pandoc 71 | pcre 72 | pcre2 73 | pinentry-mac 74 | pkg-config 75 | plantuml 76 | pth 77 | pyenv 78 | pyenv-virtualenv 79 | pyenv-virtualenvwrapper 80 | python 81 | python3 82 | rbenv 83 | rbenv-bundler 84 | readline 85 | ruby-build 86 | sbt 87 | scala 88 | screen 89 | snort 90 | sqlite 91 | squid 92 | swiftlint 93 | tig 94 | tmux 95 | tree 96 | unixodbc 97 | utf8proc 98 | watch 99 | watchman 100 | webp 101 | wget 102 | xz 103 | zsh 104 | -------------------------------------------------------------------------------- /currentUserPerm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run this script via Outset or in Self-Service. 4 | # in case you'll run manually you need to use sudo. 5 | if [[ $EUID -ne 0 ]]; then 6 | echo "This script must be run as root" 7 | exit 1 8 | fi 9 | 10 | loggedInUser=`/bin/ls -l /dev/console | /usr/bin/awk '{ print $3 }'` 11 | 12 | # Add staff to developer group 13 | dseditgroup -o edit -a staff -t group _developer 14 | 15 | # use _developer group 16 | groupScope="_developer" 17 | 18 | /bin/chmod u+rwx /usr/local/bin 19 | /bin/chmod g+rwx /usr/local/bin 20 | find /usr/local/bin/ -type l -and ! -name "jamf" -and ! -name "autopkg" -and ! -name "outset" ! -name "santactl" -exec chown -R $loggedInUser {} \; 21 | find /usr/local/bin/ -type l -and ! -name "jamf" -and ! -name "autopkg" -and ! -name "outset" ! -name "santactl" -exec chgrp -R $groupScope {} \; 22 | /bin/mkdir -p /usr/local/Cellar /usr/local/Homebrew /usr/local/Frameworks /usr/local/etc /usr/local/include /usr/local/lib /usr/local/opt /usr/local/sbin /usr/local/share /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var 23 | /bin/chmod g+rwx /usr/local/Cellar /usr/local/Homebrew /usr/local/Frameworks /usr/local/etc /usr/local/include /usr/local/lib /usr/local/opt /usr/local/sbin /usr/local/share /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var 24 | /bin/chmod 755 /usr/local/share/zsh /usr/local/share/zsh/site-functions 25 | /usr/sbin/chown -R $loggedInUser /usr/local/Cellar /usr/local/Homebrew /usr/local/Frameworks /usr/local/etc /usr/local/include /usr/local/lib /usr/local/opt /usr/local/sbin /usr/local/share /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var 26 | /usr/bin/chgrp -R $groupScope /usr/local/Cellar /usr/local/Homebrew /usr/local/Frameworks /usr/local/etc /usr/local/include /usr/local/lib /usr/local/opt /usr/local/sbin /usr/local/share /usr/local/share/zsh /usr/local/share/zsh/site-functions /usr/local/var 27 | /bin/mkdir -p /Users/$loggedInUser/Library/Caches/Homebrew 28 | /bin/chmod g+rwx /Users/$loggedInUser/Library/Caches/Homebrew 29 | /usr/sbin/chown -R $loggedInUser /Users/$loggedInUser/Library/Caches/Homebrew 30 | /bin/mkdir -p /Library/Caches/Homebrew 31 | /bin/chmod g+rwx /Library/Caches/Homebrew 32 | /usr/sbin/chown $loggedInUser /Library/Caches/Homebrew 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # brew-stew 2 | 3 | Monolithic homebrew packages for dev/build environment deployment. The goal of this project is to be able to build a single self-contained homebrew installation in the format of a macOS installer package. 4 | 5 | The package contains the entire homebrew installation at its default location of `/usr/local`, so it is designed to be built on an isolated system and the package deployed to build/dev machines _instead of_ using `brew` directly on the target system(s). It should, however, play nice with any other binaries/applications which may be installed to `/usr/local` on the target. 6 | 7 | ## Usage 8 | 9 | `brew-stew [-v] ` 10 | 11 | Use `-h` to see full help. Use `-v` to print `DEBUG` level output to stdout. `INFO` level is currently output by default. 12 | 13 | The "list_file" is simply a text file with a list of formulae. A few samples are included in the `sample_listfiles` directory. 14 | 15 | ## Approaches 16 | 17 | A couple possible approaches being played with in terms of the logic for determining what goes in the package, which are for now just termed as 'subtractive' and 'additive'. Currently the build script defaults to `additive`. 18 | 19 | ### Subtractive 20 | 21 | Building the package by packaging up all of `/usr/local`, and attempting to filter out certain items we know aren't part of brew. We can get a list of files not managed by brew using `brew ls --unbrewed`, but it seems to at least mix symlinks like `/usr/local/bin/santactl`. This is a bit of a shotgun approach and seems like it would be very easy to capture things we don't want. 22 | 23 | ### Additive 24 | 25 | Using tools like `brew ls --verbose ` to list all the files known to be installed by a formula, stage these to an alternate package root using `rsync` to preserve modes, and package from this root instead. What we don't yet have is the logic to also include symlinks or the `opt` brew directories. This may be easy to do ourselves by following naming conventions, even if brew doesn't provide an obvious way to do it. 26 | 27 | ## Reporting 28 | 29 | Currently several report files are saved in the output directory alongside the installer package: 30 | 31 | ### report.json 32 | 33 | `report.json` currently contains the following: 34 | 35 | - exhaustive output from `brew info --json=v1` 36 | - output of `santactl fileinfo` from every executable detected in the Cellar 37 | - a summary, currently containing only formula names and versions 38 | 39 | See [this wiki page](https://github.com/timsutton/brew-stew/wiki/Report-JSON) for sample JSON report output for a build of just the `cowsay` formula. 40 | 41 | ### build_debug.log 42 | 43 | Full debug output of the command, regardless of verbose level specified in the tool. Note that currently there is still a lot of output from `brew` itself which is not yet being redirected through the logger, so this currently information we're logging explicitly and nothing directly output `brew` commands. 44 | 45 | ### package_bom.txt 46 | 47 | This is the output of `lsbom` on the `Bom` file from the package, which is a complete list of all files with ownerships and modes in the package. 48 | 49 | ### formula_versions.txt 50 | 51 | This is a simple textfile with a list of formulae and versions for easy readability and diff'ing. For example: 52 | 53 | ``` 54 | dnsmasq 2.77_1 55 | docker 17.05.0 56 | jpeg 8d 57 | libtiff 4.0.8 58 | xz 5.2.3 59 | ``` 60 | -------------------------------------------------------------------------------- /brew-stew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # pylint: disable=locally-disabled, line-too-long, invalid-name 3 | 4 | import argparse 5 | import logging 6 | import json 7 | import os 8 | import re 9 | import shutil 10 | import subprocess 11 | import sys 12 | import tempfile 13 | 14 | from pprint import pprint 15 | from time import gmtime, strftime 16 | 17 | BREW_BIN = '/usr/local/bin/brew' 18 | SANTA_BIN = '/Library/Extensions/santa-driver.kext/Contents/MacOS/santactl' 19 | INSTALL_LOCATION = '/usr/local' 20 | 21 | # Some additional items which are erroneously not listed in `brew ls --unbrewed` 22 | PKG_FILTERS = [ 23 | 'bin/santactl', 24 | 'bin/osqueryctl', 25 | 'bin/osqueryd', 26 | 'bin/osqueryi', 27 | 'bin/autopkg', 28 | 'remotedesktop/RemoteDesktopChangeClientSettings.pkg', 29 | 'var', # exclude all of var to see what breaks 30 | 'Library', 31 | 'zentral', 32 | ] 33 | 34 | log = logging.getLogger('brew-stew') 35 | 36 | def cmd_output(cmd, explicit_cmd=False, env=None): 37 | '''Run a brew command passed as a list, returns (stdout, stderr) 38 | env can optionally augment the environment passed to the process, 39 | returns a 3-item tuple of (stdout, stderr, exitcode). env can 40 | augmenet the default environment.''' 41 | send_cmd = [BREW_BIN] + cmd 42 | if explicit_cmd: 43 | send_cmd = cmd 44 | new_env = os.environ.copy() 45 | new_env['HOMEBREW_NO_AUTO_UPDATE'] = '1' 46 | if env: 47 | new_env.update(env) 48 | log.debug("%s", ' '.join(send_cmd)) 49 | proc = subprocess.Popen(send_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 50 | env=new_env) 51 | out, err = proc.communicate() 52 | return (out.strip(), err, proc.returncode) 53 | 54 | def cmd_call(cmd, explicit_cmd=False, env=None): 55 | '''Just subprocess.calls the list of args to the `brew` command, 56 | returns only the process's returncode. env can augmenet the default 57 | environment.''' 58 | send_cmd = [BREW_BIN] + cmd 59 | if explicit_cmd: 60 | send_cmd = cmd 61 | new_env = os.environ.copy() 62 | new_env['HOMEBREW_NO_AUTO_UPDATE'] = '1' 63 | if env: 64 | new_env.update(env) 65 | log.debug("%s", ' '.join(send_cmd)) 66 | retcode = subprocess.call(send_cmd, env=new_env) 67 | return retcode 68 | 69 | def list_files(root, include_dirs=True): 70 | '''Returns a list of files (and optionall dirs) at a given root. Useful 71 | for feeding to stage_files().''' 72 | file_list = [] 73 | for root, dirs, files in os.walk(root): 74 | if include_dirs: 75 | for d in dirs: 76 | file_list.append(os.path.join(root, d)) 77 | for f in files: 78 | file_list.append(os.path.join(root, f)) 79 | return file_list 80 | 81 | def stage_files(source_file_list, pkgroot, opts=['-a']): 82 | file_list_path = tempfile.mkstemp()[1] 83 | with open(file_list_path, 'w') as fd: 84 | fd.write('\n'.join(source_file_list)) 85 | rsync_cmd = ['/usr/bin/rsync'] + opts 86 | rsync_cmd += ['--files-from', file_list_path, '/', pkgroot] 87 | cmd_call(rsync_cmd, explicit_cmd=True) 88 | 89 | class BrewStewEnv(object): 90 | def __init__(self, brew_file, output_dir): 91 | cmd_call(['analytics', 'off']) 92 | 93 | self.brew_list = [] 94 | for line in open(brew_file, 'r').read().splitlines(): 95 | if line.startswith("#"): 96 | continue 97 | self.brew_list.append(line) 98 | self.output_dir = output_dir 99 | 100 | self.installed_json = [] 101 | self.installed_formulae = [] 102 | self._update_installed() 103 | 104 | self.non_homebrew_files = [] 105 | self._update_unbrewed() 106 | 107 | self.prefix, _, _ = cmd_output(['--prefix']) 108 | self.cellar, _, _ = cmd_output(['--cellar']) 109 | self.filtered_pkg_files = [] 110 | 111 | self.pkg_version = strftime('%Y.%m.%d', gmtime()) 112 | self.built_pkg_path = os.path.join(self.output_dir, 'brew-stew-%s.pkg' % self.pkg_version) 113 | 114 | def _update_installed(self): 115 | installed = [] 116 | info_json, _, _ = cmd_output(['info', '--json=v1', '--installed']) 117 | self.installed_json = json.loads(info_json) 118 | for f in self.installed_json: 119 | installed.append((f['name'], f['installed'][0]['version'])) 120 | if len(f['installed']) > 1: 121 | log.error("WARNING: Formula %s has more than one version installed, unexpected", f['name']) 122 | self.installed_formulae = installed 123 | 124 | def _update_unbrewed(self): 125 | proc = subprocess.Popen([BREW_BIN, 'ls', '--unbrewed'], stdout=subprocess.PIPE) 126 | out, _ = proc.communicate() 127 | for line in out.splitlines(): 128 | self.non_homebrew_files.append(line) 129 | 130 | def brew_outdated(self): 131 | cmd_call(['outdated', '--json=v1']) # print for debugging 132 | 133 | def brew_update(self): 134 | cmd_call(['update']) 135 | 136 | def brew_upgrade(self): 137 | cmd_call(['upgrade']) 138 | 139 | def brew_install(self): 140 | log.info("Beginning brew install of formulae: %s", ', '.join(self.brew_list)) 141 | for brew in self.brew_list: 142 | cmd_call(['install', brew]) 143 | self._update_installed() 144 | self._update_unbrewed() 145 | 146 | def brew_test(self): 147 | for brew in self.brew_list: 148 | cmd_call(['test', brew]) 149 | 150 | def cleanroom(self): 151 | if self.installed_formulae: 152 | rm_cmd = ['rm', '--force', '--ignore-dependencies'] 153 | rm_cmd.extend([f for (f, _) in self.installed_formulae]) 154 | cmd_call(rm_cmd) 155 | cmd_call(['cleanup']) 156 | 157 | def build_pkg(self, strategy='additive'): 158 | pkgbuild_cmd = ['/usr/bin/pkgbuild', '--install-location', INSTALL_LOCATION, '--identifier', 'org.brew-stew.pkg', '--version', self.pkg_version] 159 | 160 | log.info("Initiating pkg build using '%s' strategy", strategy) 161 | if strategy == 'subtractive': 162 | pkgbuild_cmd += ['--root', self.prefix] 163 | # except we get warnings for these files, presumably because of the ++: 164 | # WARNING **** Can't compile pattern: share/mime/text/x-c++src.xml 165 | self.filtered_pkg_files += self.non_homebrew_files 166 | self.filtered_pkg_files += PKG_FILTERS 167 | self.filtered_pkg_files += [ 168 | '.DS_Store', 169 | '.git', 170 | 'Homebrew', # brew core git repos 171 | 'bin/brew', # brew CLI binstub 172 | ] 173 | for pattern in self.filtered_pkg_files: 174 | pkgbuild_cmd += ['--filter', pattern] 175 | pkgbuild_cmd += [self.built_pkg_path] 176 | 177 | 178 | if strategy == 'additive': 179 | pkgroot = tempfile.mkdtemp() 180 | # TODO: derive this from a variable/const 181 | pkgbuild_cmd += ['--root', os.path.join(pkgroot, 'usr/local')] 182 | 183 | file_list = cmd_output(['ls', '--verbose'] + [name for (name, _) in self.installed_formulae])[0].splitlines() 184 | stage_files(file_list, pkgroot) 185 | 186 | # recursively walk the brew prefix to find links of interest, according to this criteria, 187 | # in this order: 188 | # - path of the symlink isn't excluded by exclude_re 189 | # - the target of the symlink matches include_re - note that the 190 | # targets are often relative ('../Cellar/..') but not always; npm 191 | # is a symlink to '/usr/local/lib/node_modules/npm/bin/npm-cli.js' for example, 192 | # so adding other paths to include_re is a TODO 193 | # 194 | # TODO: 195 | # - could there still be anything here that's stale, i.e. from a 196 | # previous formula that's no longer in our install list? if so, 197 | # we could probably also add a check that any included item has 198 | # to contain a path like 'Cellar/' so that we can count 199 | # on it being relevant 200 | exclude_re = r'^\/usr\/local\/(bin\/brew|Homebrew).*$' 201 | include_re = r'^(\/usr\/local\/lib.*|.*Cellar).*$' 202 | symlinks = [] 203 | log.info("Locating symlinks in Homebrew directory") 204 | for root, _, files in os.walk(INSTALL_LOCATION): 205 | if re.match(exclude_re, root): 206 | continue 207 | for f in files: 208 | full_spath = os.path.join(root, f) 209 | if os.path.islink(full_spath): 210 | if not re.match(include_re, os.path.realpath(full_spath)): 211 | continue 212 | linked_path = os.readlink(full_spath) 213 | log.debug("Got link '%s' --> '%s'", full_spath, linked_path) 214 | symlinks.append(full_spath) 215 | log.info("Staging symlinks") 216 | stage_files(symlinks, pkgroot) 217 | 218 | additional_stage_dirs = ['opt', 'var'] 219 | for add_dir in additional_stage_dirs: 220 | log.info("Staging additional dir in pkg: '%s'", add_dir) 221 | files_to_add = list_files(os.path.join('/usr/local', add_dir)) 222 | stage_files(files_to_add, pkgroot) 223 | 224 | log.debug("Calling pkgbuild command: %s", pkgbuild_cmd) 225 | pkgbuild_cmd.append(self.built_pkg_path) 226 | subprocess.call(pkgbuild_cmd) 227 | 228 | log.debug("Cleaning up temp root dir at %s", pkgroot) 229 | shutil.rmtree(pkgroot) 230 | 231 | def dump_pkg_files(self): 232 | subprocess.call(['/usr/sbin/pkgutil', '--payload-files', self.built_pkg_path]) 233 | 234 | def build_report(self): 235 | '''Write a report.json from this package run out to the current directory''' 236 | report = {} 237 | # summary will contain a simple dictionary of formulae with versions for ease 238 | # of parsing 239 | report['summary'] = {} 240 | report['summary']['formulae'] = [] 241 | 242 | report['formulae'] = [] 243 | for formula in self.brew_list: 244 | log.debug("Gathering report for formula '%s'", formula) 245 | # Every item in self.brew_list may not have actually installed successfully, 246 | # so first make sure we've got a matching entry from `brew info --installed` 247 | # 248 | # TODO: see if we can't just instead use `self.installed_formulae` for this.. 249 | try: 250 | brew_info = [item for item in self.installed_json if item['name'] == formula][0] 251 | except IndexError: 252 | continue 253 | 254 | f = {} 255 | f['name'] = formula 256 | # Direct output of `brew info --json=v1` 257 | f['brew_info'] = brew_info 258 | # I don't know when 'installed' _wouldn't_ be present, but let's be 259 | # cautious anyway 260 | if f['brew_info'].get('installed'): 261 | report['summary']['formulae'].append( 262 | {'name': f['name'], 263 | 'version': f['brew_info']['installed'][0]['version'], 264 | }) 265 | else: 266 | log.warning("WARNING: unexpected missing 'installed' dictionary in brew info for formula '%s'", formula) 267 | 268 | # Direct output of `santactl fileinfo --json` for all binaries in this 269 | # formula's cellar 270 | f['santa_info'] = [] 271 | # find any executables with this formula's Cellar location 272 | for root, _, files in os.walk(os.path.join(self.cellar, formula)): 273 | for phile in files: 274 | full_path = os.path.join(root, phile) 275 | if os.path.isfile(full_path) and os.access(full_path, os.X_OK): 276 | santa_cmd = [ 277 | SANTA_BIN, 278 | 'fileinfo', 279 | '--json', 280 | full_path] 281 | santa_out, santa_err, _ = cmd_output(santa_cmd, explicit_cmd=True) 282 | # santactl doesn't output a non-zero exit code on an 'Invalid or empty file' error, 283 | # so let's try scraping stderr 284 | if santa_err: 285 | log.warning("santactl error on executing %s: '%s' - santa output for this binary will be skipped", 286 | santa_cmd, santa_err) 287 | continue 288 | santa_json = json.loads(santa_out) 289 | f['santa_info'].append(santa_json[0]) 290 | report['formulae'].append(f) 291 | 292 | with open(os.path.join(self.output_dir, 'report.json'), 'w') as fd: 293 | json.dump(report, fd, indent=2) 294 | 295 | # Dump report of additional reporting items: 296 | # - textfile with simple "formula version" 297 | versions_txt_path = os.path.join(self.output_dir, 'formula_versions.txt') 298 | with open(versions_txt_path, 'w') as fd: 299 | for formula, ver in self.installed_formulae: 300 | fd.write("%s %s\n" % (formula, ver)) 301 | log.info("Wrote formula versions textfile to %s", versions_txt_path) 302 | 303 | # - full Bom output 304 | bom_report_path = os.path.join(self.output_dir, 'package_bom.txt') 305 | bom_tmp, _, _ = cmd_output(['/usr/sbin/pkgutil', '--bom', self.built_pkg_path], 306 | explicit_cmd=True) 307 | bom_out, _, _ = cmd_output(['/usr/bin/lsbom', bom_tmp], explicit_cmd=True) 308 | with open(bom_report_path, 'w') as fd: 309 | fd.write(bom_out) 310 | log.info("Write installer package BOM (bill of materials) output to %s", bom_report_path) 311 | 312 | def main(): 313 | desc = """Builds monolithic macOS installer packages from a Homebrew formula 314 | install configuration. Given a list of formulae in a text file and an 315 | output directory, it outputs a .pkg and several report files.""" 316 | 317 | parser = argparse.ArgumentParser(description=desc) 318 | parser.add_argument( 319 | 'brew_list_file', type=str, 320 | help="Path to a file containing a list of formulae") 321 | parser.add_argument( 322 | 'output_dir', type=str, 323 | help="Path to the output directory for all build files") 324 | parser.add_argument( 325 | '--verbose', '-v', action='count', default=0, 326 | help=("Increase output verbosity, currently can be specified only once")) 327 | args = parser.parse_args() 328 | if args.verbose > 1: 329 | sys.exit("Currently the --verbose option can only be specified once, enabling DEBUG-level output") 330 | if not os.path.exists(args.brew_list_file): 331 | sys.exit("brew list file %s is invalid or can't be found." % args.brew_list_file) 332 | if not os.path.exists(args.output_dir): 333 | os.makedirs(args.output_dir) 334 | if not os.path.exists(SANTA_BIN): 335 | sys.exit("brew-stew requires an installation of Google Santa on this " 336 | "build machine for the purposes of generating reports. Please " 337 | "install the latest Santa release package from " 338 | "https://github.com/google/santa/releases and re-run.") 339 | 340 | # global logger needs the lowest debug log enabled, but then we may raise the stdout (StreamHandler) 341 | # handler back up according to user options. build_debug.log always uses DEBUG level. 342 | log.setLevel(logging.DEBUG) 343 | 344 | formatter = logging.Formatter('%(asctime)s [%(levelname)s] - (%(funcName)s): %(message)s') 345 | 346 | stream_handler = logging.StreamHandler(stream=sys.stdout) 347 | stream_handler.setLevel(logging.INFO - (10 * args.verbose)) 348 | file_handler = logging.FileHandler(os.path.join(args.output_dir, 'build_debug.log')) 349 | file_handler.setLevel(logging.DEBUG) 350 | 351 | # set both stdout and file logging to use the same output format and attach to the 352 | # same global logger 353 | for handler in [stream_handler, file_handler]: 354 | handler.setFormatter(formatter) 355 | log.addHandler(handler) 356 | 357 | env = BrewStewEnv(args.brew_list_file, args.output_dir) 358 | env.cleanroom() 359 | env.brew_update() 360 | env.brew_install() 361 | env.brew_test() 362 | 363 | env.build_pkg() 364 | 365 | env.build_report() 366 | 367 | 368 | if __name__ == '__main__': 369 | main() 370 | --------------------------------------------------------------------------------