| #!/usr/bin/python |
| # |
| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| # |
| """Generate an HTML file containing license info for all installed packages. |
| |
| Documentation on this script is also available here: |
| http://www.chromium.org/chromium-os/licensing-for-chromiumos-developers |
| |
| End user (i.e. package owners) documentation is here: |
| http://www.chromium.org/chromium-os/licensing-for-chromiumos-package-owners |
| |
| Usage: |
| For this script to work, you must have built the architecture |
| this is being run against, _after_ you've last run repo sync. |
| Otherwise, it will query newer source code and then fail to work on packages |
| that are out of date in your build. |
| |
| Recommended build: |
| cros_sdk |
| export BOARD=x86-alex |
| sudo rm -rf /build/$BOARD |
| cd ~/trunk/src/scripts |
| # If you wonder why we need to build Chromium OS just to run |
| # `emerge -p -v virtual/target-os` on it, we don't. |
| # However, later we run ebuild unpack, and this will apply patches and run |
| # configure. Configure will fail due to aclocal macros missing in |
| # /build/x86-alex/usr/share/aclocal (those are generated during build). |
| # This will take about 10mn on a Z620. |
| ./build_packages --board=$BOARD --nowithautotest --nowithtest --nowithdev |
| --nowithfactory |
| cd ~/trunk/chromite/licensing |
| # This removes left over packages from an earlier build that could cause |
| # conflicts. |
| eclean-$BOARD packages |
| %(prog)s [--debug] [--all-packages] --board $BOARD [-o o.html] 2>&1 | tee out |
| |
| The workflow above is what you would do to generate a licensing file by hand |
| given a chromeos tree. |
| Note that building packages now creates a license.yaml fork in the package |
| which you can see with |
| qtbz2 -x -O /build/x86-alex/packages/dev-util/libc-bench-0.0.1-r8.tbz2 | |
| qxpak -x -O - license.yaml |
| This gets automatically installed in |
| /build/x86-alex/var/db/pkg/dev-util/libc-bench-0.0.1-r8/license.yaml |
| |
| Unless you run with --generate, the script will now gather those license |
| bits and generate a license file from there. |
| License bits for each package are generated by default from |
| src/scripts/hooks/install/gen-package-licenses.sh which gets run automatically |
| by emerge as part of a package build (by running this script with |
| --hook /path/to/tmp/portage/build/tree/for/that/package |
| |
| If license bits are missing, they are generated on the fly if you were running |
| with sudo. If you didn't use sudo, this on the fly late generation will fail |
| and act as a warning that your prebuilts were missing package build time |
| licenses. |
| |
| You can check the licenses and/or generate a HTML file for a list of |
| packages using --package or -p: |
| %(prog)s --package "dev-libs/libatomic_ops-7.2d" --package |
| "net-misc/wget-1.14" --board $BOARD -o out.html |
| |
| Note that you'll want to use --generate to force regeneration of the licensing |
| bits from a package source you may have just modified but not rebuilt. |
| |
| If you want to check licensing against all ChromeOS packages, you should |
| run ./build_packages --board=$BOARD to build everything and then run |
| this script with --all-packages. |
| |
| By default, when no package is specified, this script processes all |
| packages for $BOARD. The output HTML file is meant to update |
| http://src.chromium.org/viewvc/chrome/trunk/src/chrome/browser/resources/ + |
| chromeos/about_os_credits.html?view=log |
| (gclient config svn://svn.chromium.org/chrome/trunk/src) |
| For an example CL, see https://codereview.chromium.org/13496002/ |
| |
| The detailed process is listed below. |
| |
| * Check out the branch you intend to generate the HTML file for. Use |
| the internal manifest for this purpose. |
| repo init -b <branch_name> -u <URL> |
| |
| The list of branches (e.g. release-R33-5116.B) are available here: |
| https://chromium.googlesource.com/chromiumos/manifest/+refs |
| |
| * Generate the HTML file by following the steps mentioned |
| previously. Check whether your changes are valid with: |
| bin/diff_license_html output.html-M33 output.html-M34 |
| and review the diff. |
| |
| * Update the about_os_credits.html in the svn repository. Create a CL |
| and upload it for review. |
| gcl change <change_name> |
| gcl upload <change_name> |
| |
| When uploading, you may get a warning for file being too large to |
| upload. In this case, your CL can still be reviewed. Always include |
| the diff in your commit message so that the reviewers know what the |
| changes are. You can add reviewers on the review page by clicking on |
| "Edit issue". (A quick reference: |
| http://www.chromium.org/developers/quick-reference) |
| |
| Make sure you click on 'Publish+Mail Comments' after adding reviewers |
| (the review URL looks like this https://codereview.chromium.org/183883018/ ). |
| |
| * After receiving LGTMs, commit your change with 'gcl commit <change_name>'. |
| |
| If you don't get this in before the freeze window, it'll need to be merged into |
| the branch being released, which is done by adding a Merge-Requested label. |
| Once it's been updated to "Merge-Approved" by a TPM, please merge into the |
| required release branch. You can ask karen@ for merge approve help. |
| Example: http://crbug.com/221281 |
| |
| Note however that this is only during the transition period. |
| build-image will be modified to generate the license for each board and save |
| the file in /opt/google/chrome/resources/about_os_credits.html or as defined |
| in http://crbug.com/271832 . |
| """ |
| |
| import cgi |
| import codecs |
| import logging |
| import os |
| import re |
| import tempfile |
| |
| from chromite.cbuildbot import constants |
| from chromite.cbuildbot import portage_utilities |
| from chromite.lib import commandline |
| from chromite.lib import cros_build_lib |
| from chromite.lib import osutils |
| |
| # We are imported by src/repohooks/pre-upload.py in a non chroot environment |
| # where yaml may not be there, so we don't error on that since it's not needed |
| # in that case. |
| try: |
| import yaml |
| except ImportError: |
| yaml = None |
| |
| debug = False |
| |
| # See http://crbug.com/207004 for discussion. |
| PER_PKG_LICENSE_DIR = '/var/db/pkg' |
| |
| STOCK_LICENSE_DIRS = [ |
| os.path.join(constants.SOURCE_ROOT, |
| 'src/third_party/portage-stable/licenses'), |
| ] |
| |
| # There are licenses for custom software we got and isn't part of |
| # upstream gentoo. |
| CUSTOM_LICENSE_DIRS = [ |
| os.path.join(constants.SOURCE_ROOT, |
| 'src/third_party/chromiumos-overlay/licenses'), |
| ] |
| |
| COPYRIGHT_ATTRIBUTION_DIR = ( |
| os.path.join( |
| constants.SOURCE_ROOT, |
| 'src/third_party/chromiumos-overlay/licenses/copyright-attribution')) |
| |
| # Virtual packages don't need to have a license and often don't, so we skip them |
| # chromeos-base contains google platform packages that are covered by the |
| # general license at top of tree, so we skip those too. |
| SKIPPED_CATEGORIES = [ |
| 'virtual', |
| ] |
| |
| SKIPPED_PACKAGES = [ |
| # Fix these packages by adding a real license in the code. |
| # You should not skip packages just because the license scraping doesn't |
| # work. Stick those special cases into PACKAGE_LICENSES. |
| # Packages should only be here because they are sub/split packages already |
| # covered by the license of the main package. |
| |
| # These are Chrome-OS-specific packages, copyright BSD-Google |
| 'sys-kernel/chromeos-kernel', # already manually credit Linux |
| ] |
| |
| LICENSE_NAMES_REGEX = [ |
| r'^copyright$', |
| r'^copyright[.]txt$', |
| r'^copyright[.]regex$', # llvm |
| r'^copying.*$', |
| r'^licen[cs]e.*$', |
| r'^licensing.*$', # libatomic_ops |
| r'^ipa_font_license_agreement_v1[.]0[.]txt$', # ja-ipafonts |
| r'^PKG-INFO$', # copyright assignment for |
| # some python packages |
| # (netifaces, unittest2) |
| ] |
| |
| # These are _temporary_ license mappings for packages that do not have a valid |
| # shared/custom license, or LICENSE file we can use. |
| # Once this script runs earlier (during the package build process), it will |
| # block new source without a LICENSE file if the ebuild contains a license |
| # that requires copyright assignment (BSD and friends). |
| # At that point, new packages will get fixed to include LICENSE instead of |
| # adding workaround mappings like those below. |
| # The way you now fix copyright attribution cases create a custom file with the |
| # right license directly in COPYRIGHT_ATTRIBUTION_DIR. |
| PACKAGE_LICENSES = { |
| # TODO: replace the naive license parsing code in this script with a hook |
| # into portage's license parsing. See http://crbug.com/348779 |
| |
| # Chrome (the browser) is complicated, it has a morphing license that is |
| # either BSD-Google, or BSD-Google,Google-TOS depending on how it was |
| # built. We bypass this problem for now by hardcoding the Google-TOS bit as |
| # per ChromeOS with non free bits |
| 'chromeos-base/chromeos-chrome': ['BSD-Google', 'Google-TOS'], |
| |
| # Currently the code cannot parse LGPL-3 || ( LGPL-2.1 MPL-1.1 ) |
| 'dev-python/pycairo': ['LGPL-3', 'LGPL-2.1'], |
| } |
| |
| # Any license listed list here found in the ebuild will make the code look for |
| # license files inside the package source code in order to get copyright |
| # attribution from them. |
| COPYRIGHT_ATTRIBUTION_LICENSES = [ |
| 'BSD', # requires distribution of copyright notice |
| 'BSD-2', # so does BSD-2 http://opensource.org/licenses/BSD-2-Clause |
| 'BSD-3', # and BSD-3? http://opensource.org/licenses/BSD-3-Clause |
| 'BSD-4', # and 4? |
| 'BSD-with-attribution', |
| 'MIT', |
| 'MIT-with-advertising', |
| 'Old-MIT', |
| ] |
| |
| # The following licenses are not invalid or to show as a less helpful stock |
| # license, but it's better to look in the source code for a more specific |
| # license if there is one, but not an error if no better one is found. |
| # Note that you don't want to set just anything here since any license here |
| # will be included once in stock form and a second time in custom form if |
| # found (there is no good way to know that a license we found on disk is the |
| # better version of the stock version, so we show both). |
| LOOK_IN_SOURCE_LICENSES = [ |
| 'as-is', # The stock license is very vague, source always has more details. |
| 'PSF-2', # The custom license in python is more complete than the template. |
| |
| # As far as I know, we have no requirement to do copyright attribution for |
| # these licenses, but the license included in the code has slightly better |
| # information than the stock Gentoo one (including copyright attribution). |
| 'BZIP2', # Single use license, do copyright attribution. |
| 'OFL', # Almost single use license, do copyright attribution. |
| 'OFL-1.1', # Almost single use license, do copyright attribution. |
| 'UoI-NCSA', # Only used by NSCA, might as well show their custom copyright. |
| ] |
| |
| # This used to provide overrides. I can't find a valid reason to add any more |
| # here, though. |
| PACKAGE_HOMEPAGES = { |
| # Example: |
| # 'x11-proto/glproto': ['http://www.x.org/'], |
| } |
| |
| # These are tokens found in LICENSE= in an ebuild that aren't licenses we |
| # can actually read from disk. |
| # You should not use this to blacklist real licenses. |
| LICENCES_IGNORE = [ |
| ')', # Ignore OR tokens from LICENSE="|| ( LGPL-2.1 MPL-1.1 )" |
| '(', |
| '||', |
| ] |
| |
| TMPL = 'about_credits.tmpl' |
| ENTRY_TMPL = 'about_credits_entry.tmpl' |
| SHARED_LICENSE_TMPL = 'about_credits_shared_license_entry.tmpl' |
| |
| |
| # This is called directly by src/repohooks/pre-upload.py |
| def GetLicenseTypesFromEbuild(ebuild_path): |
| """Returns a list of license types from the ebuild file. |
| |
| This function does not always return the correct list, but it is |
| faster than using portageq for not having to access chroot. It is |
| intended to be used for tasks such as presubmission checks. |
| |
| Args: |
| ebuild_path: ebuild to read. |
| |
| Returns: |
| list of licenses read from ebuild. |
| |
| Raises: |
| ValueError: ebuild errors. |
| """ |
| ebuild_env_tmpl = """ |
| has() { [[ " ${*:2} " == *" $1 "* ]]; } |
| inherit() { |
| local overlay_list="%(overlay_list)s" |
| local eclass overlay f |
| for eclass; do |
| has ${eclass} ${_INHERITED_} && continue |
| _INHERITED_+=" ${eclass}" |
| for overlay in %(overlay_list)s; do |
| f="${overlay}/eclass/${eclass}.eclass" |
| if [[ -e ${f} ]]; then |
| source "${f}" |
| break |
| fi |
| done |
| done |
| } |
| source %(ebuild)s""" |
| |
| # TODO: the overlay_list hard-coded here should be changed to look |
| # at the current overlay, and then the master overlays. E.g. for an |
| # ebuild file in overlay-parrot, we will look at parrot overlay |
| # first, and then look at portage-stable and chromiumos, which are |
| # listed as masters in overlay-parrot/metadata/layout.conf. |
| tmpl_env = { |
| 'ebuild': ebuild_path, |
| 'overlay_list': '%s %s' % ( |
| os.path.join(constants.SOURCE_ROOT, |
| 'src/third_party/chromiumos-overlay'), |
| os.path.join(constants.SOURCE_ROOT, |
| 'src/third_party/portage-stable')) |
| } |
| |
| with tempfile.NamedTemporaryFile(bufsize=0) as f: |
| osutils.WriteFile(f.name, ebuild_env_tmpl % tmpl_env) |
| env = osutils.SourceEnvironment( |
| f.name, whitelist=['LICENSE'], ifs=' ', multiline=True) |
| |
| if not env.get('LICENSE'): |
| raise ValueError('No LICENSE found in the ebuild.') |
| if re.search(r'[,;]', env['LICENSE']): |
| raise ValueError( |
| 'LICENSE field in the ebuild should be whitespace-limited.') |
| |
| return env['LICENSE'].split() |
| |
| |
| class PackageLicenseError(Exception): |
| """Thrown if something fails while getting license information for a package. |
| |
| This will cause the processing to error in the end. |
| """ |
| |
| |
| class PackageInfo(object): |
| """Package info containers, mostly for storing licenses.""" |
| |
| def __init__(self): |
| |
| self.board = None |
| self.revision = None |
| |
| # Array of scanned license texts. |
| self.license_text_scanned = [] |
| |
| self.category = None |
| self.name = None |
| self.version = None |
| |
| # Looks something like this |
| # /mnt/host/source/src/ |
| # third_party/portage-stable/net-misc/rsync/rsync-3.0.8.ebuild |
| self.ebuild_path = None |
| |
| # Array of license names retrieved from ebuild or override in this code. |
| self.ebuild_license_names = [] |
| self.homepages = [] |
| # This contains licenses names we can read from Gentoo or custom licenses. |
| # These are supposed to be shared licenses (i.e. licenses referenced by |
| # more then one package), but after all processing, we may find out that |
| # some are only used once and they get taken out of the shared pool and |
| # pasted directly in the sole package that was using them (see |
| # GenerateHTMLLicenseOutput). |
| self.license_names = set() |
| |
| # We set this if the ebuild has a BSD/MIT like license that requires |
| # scanning for a LICENSE file in the source code, or a static mapping |
| # in PACKAGE_LICENSES. Not finding one once this is set, is fatal. |
| self.need_copyright_attribution = False |
| # This flag just says we'd like to include licenses from the source, but |
| # not finding any is not fatal. |
| self.scan_source_for_licenses = False |
| |
| # After reading basic package information, we can mark the package as |
| # one to skip in licensing. |
| self.skip = False |
| |
| # If we failed to get licensing for this package, mark it as such so that |
| # it can be flagged when the full license file is being generated. |
| self.licensing_failed = False |
| |
| # If we are called from a hook, we grab package info from the soure tree. |
| # This is also used as a flag to know whether we should do package work |
| # based on an installed package, or one that is being built and we got |
| # called from the hook. |
| self.build_source_tree = None |
| |
| @property |
| def fullnamerev(self): |
| s = '%s-%s' % (self.fullname, self.version) |
| if self.revision: |
| s += '-r%s' % self.revision |
| return s |
| |
| @property |
| def fullname(self): |
| return '%s/%s' % (self.category, self.name) |
| |
| @property |
| def license_dump_path(self): |
| """e.g. /build/x86-alex//var/db/pkg/sys-apps/dtc-1.4.0/license.yaml.""" |
| return "%s/%s/%s/license.yaml" % (cros_build_lib.GetSysroot(self.board), |
| PER_PKG_LICENSE_DIR, self.fullnamerev) |
| |
| def _BuildInfo(self, filename): |
| filename = '%s/build-info/%s' % (self.build_source_tree, filename) |
| # Buildinfo properties we read are in US-ASCII, not Unicode. |
| try: |
| bi = open(filename).read().rstrip() |
| # Some properties like HOMEPAGE may be absent. |
| except IOError: |
| bi = "" |
| return bi |
| |
| def _RunEbuildPhases(self, phases): |
| """Run a list of ebuild phases on an ebuild. |
| |
| Args: |
| phases: list of phases like ['clean', 'fetch'] or ['unpack']. |
| |
| Returns: |
| ebuild command output |
| """ |
| |
| return cros_build_lib.RunCommand( |
| ['ebuild-%s' % self.board, self.ebuild_path] + phases, print_cmd=debug, |
| redirect_stdout=True) |
| |
| def _GetOverrideLicense(self): |
| """Look in COPYRIGHT_ATTRIBUTION_DIR for license with copyright attribution. |
| |
| For dev-util/bsdiff-4.3-r5, the code will look for |
| dev-util/bsdiff-4.3-r5 |
| dev-util/bsdiff-4.3 |
| dev-util/bsdiff |
| |
| It is ok to have more than one bsdiff license file, and an empty file acts |
| as a rubout (i.e. an empty dev-util/bsdiff-4.4 will shadow dev-util/bsdiff |
| and tell the licensing code to look in the package source for a license |
| instead of using dev-util/bsdiff as an override). |
| |
| Returns: |
| False (no license found) or a multiline license string. |
| """ |
| license_read = None |
| # dev-util/bsdiff-4.3-r5 -> bsdiff-4.3-r5 |
| filename = os.path.basename(self.fullnamerev) |
| license_path = os.path.join(COPYRIGHT_ATTRIBUTION_DIR, |
| os.path.dirname(self.fullnamerev)) |
| pv = portage_utilities.SplitPV(filename) |
| pv_no_rev = '%s-%s' % (pv.package, pv.version_no_rev) |
| for filename in (pv.pv, pv_no_rev, pv.package): |
| file_path = os.path.join(license_path, filename) |
| logging.debug("Looking for override copyright attribution license in %s", |
| file_path) |
| if os.path.exists(file_path): |
| # Turn |
| # /../merlin/trunk/src/third_party/chromiumos-overlay/../dev-util/bsdiff |
| # into |
| # chromiumos-overlay/../dev-util/bsdiff |
| short_dir_path = os.path.join(*file_path.rsplit(os.path.sep, 5)[1:]) |
| license_read = "Copyright Attribution License %s:\n\n" % short_dir_path |
| license_read += ReadUnknownEncodedFile( |
| file_path, "read copyright attribution license") |
| break |
| |
| return license_read |
| |
| def _ExtractLicenses(self): |
| """Scrounge for text licenses in the source of package we'll unpack. |
| |
| This is only called if we couldn't get usable licenses from the ebuild, |
| or one of them is BSD/MIT like which forces us to look for a file with |
| copyright attribution in the source code itself. |
| |
| First, we have a shortcut where we scan COPYRIGHT_ATTRIBUTION_DIR to see if |
| we find a license for this package. If so, we use that. |
| Typically it'll be used if the unpacked source does not have the license |
| that we're required to display for copyright attribution (in some cases it's |
| plain absent, in other cases, it could be in a filename we don't look for). |
| |
| Otherwise, we scan the unpacked source code for what looks like license |
| files as defined in LICENSE_NAMES_REGEX. |
| |
| Raises: |
| AssertionError: on runtime errors |
| PackageLicenseError: couldn't find copyright attribution file. |
| """ |
| license_override = self._GetOverrideLicense() |
| if license_override: |
| self.license_text_scanned = [license_override] |
| return |
| |
| if self.build_source_tree: |
| workdir = "%s/work" % self.build_source_tree |
| else: |
| self._RunEbuildPhases(['clean', 'fetch']) |
| output = self._RunEbuildPhases(['unpack']).output.splitlines() |
| # Output is spammy, it looks like this: |
| # * gc-7.2d.tar.gz RMD160 SHA1 SHA256 size ;-) ... [ ok ] |
| # * checking gc-7.2d.tar.gz ;-) ... [ ok ] |
| # * Running stacked hooks for pre_pkg_setup |
| # * sysroot_build_bin_dir ... |
| # [ ok ] |
| # * Running stacked hooks for pre_src_unpack |
| # * python_multilib_setup ... |
| # [ ok ] |
| # >>> Unpacking source... |
| # >>> Unpacking gc-7.2d.tar.gz to /build/x86-alex/tmp/po/[...]ps-7.2d/work |
| # >>> Source unpacked in /build/x86-alex/tmp/portage/[...]ops-7.2d/work |
| # So we only keep the last 2 lines, the others we don't care about. |
| output = [line for line in output if line[0:3] == ">>>" and |
| line != ">>> Unpacking source..."] |
| for line in output: |
| logging.info(line) |
| |
| args = ['portageq-%s' % self.board, 'envvar', 'PORTAGE_TMPDIR'] |
| result = cros_build_lib.RunCommand(args, print_cmd=debug, |
| redirect_stdout=True) |
| tmpdir = result.output.splitlines()[0] |
| # tmpdir gets something like /build/daisy/tmp/ |
| workdir = os.path.join(tmpdir, 'portage', self.fullnamerev, 'work') |
| |
| if not os.path.exists(workdir): |
| raise AssertionError("Unpack of %s didn't create %s. Version mismatch" % |
| (self.fullnamerev, workdir)) |
| |
| # You may wonder how deep should we go? |
| # In case of packages with sub-packages, it could be deep. |
| # Let's just be safe and get everything we can find. |
| # In the case of libatomic_ops, it's actually required to look deep |
| # to find the MIT license: |
| # dev-libs/libatomic_ops-7.2d/work/gc-7.2/libatomic_ops/doc/LICENSING.txt |
| args = ['find', workdir, '-type', 'f'] |
| result = cros_build_lib.RunCommand(args, print_cmd=debug, |
| redirect_stdout=True).output.splitlines() |
| # Truncate results to look like this: swig-2.0.4/COPYRIGHT |
| files = [x[len(workdir):].lstrip('/') for x in result] |
| license_files = [] |
| for name in files: |
| # When we scan a source tree managed by git, this can contain license |
| # files that are not part of the source. Exclude those. |
| # (e.g. .git/refs/heads/licensing) |
| if ".git/" in name: |
| continue |
| basename = os.path.basename(name) |
| # Looking for license.* brings up things like license.gpl, and we |
| # never want a GPL license when looking for copyright attribution, |
| # so we skip them here. We also skip regexes that can return |
| # license.py (seen in some code). |
| if re.search(r".*GPL.*", basename) or re.search(r"\.py$", basename): |
| continue |
| for regex in LICENSE_NAMES_REGEX: |
| if re.search(regex, basename, re.IGNORECASE): |
| license_files.append(name) |
| break |
| |
| if not license_files: |
| if self.need_copyright_attribution: |
| logging.error(""" |
| %s: unable to find usable license. |
| Typically this will happen because the ebuild says it's MIT or BSD, but there |
| was no license file that this script could find to include along with a |
| copyright attribution (required for BSD/MIT). |
| |
| If this is Google source, please change |
| LICENSE="BSD" |
| to |
| LICENSE="BSD-Google" |
| |
| If not, go investigate the unpacked source in %s, |
| and find which license to assign. Once you found it, you should copy that |
| license to a file under %s |
| (or you can modify LICENSE_NAMES_REGEX to pickup a license file that isn't |
| being scraped currently).""", |
| self.fullnamerev, workdir, COPYRIGHT_ATTRIBUTION_DIR) |
| raise PackageLicenseError() |
| else: |
| # We can get called for a license like as-is where it's preferable |
| # to find a better one in the source, but not fatal if we didn't. |
| logging.info("Was not able to find a better license for %s " |
| "in %s to replace the more generic one from ebuild", |
| self.fullnamerev, workdir) |
| |
| # Examples of multiple license matches: |
| # dev-lang/swig-2.0.4-r1: swig-2.0.4/COPYRIGHT swig-2.0.4/LICENSE |
| # dev-libs/glib-2.32.4-r1: glib-2.32.4/COPYING pkg-config-0.26/COPYING |
| # dev-libs/libnl-3.2.14: libnl-doc-3.2.14/COPYING libnl-3.2.14/COPYING |
| # dev-libs/libpcre-8.30-r2: pcre-8.30/LICENCE pcre-8.30/COPYING |
| # dev-libs/libusb-0.1.12-r6: libusb-0.1.12/COPYING libusb-0.1.12/LICENSE |
| # dev-libs/pyzy-0.1.0-r1: db/COPYING pyzy-0.1.0/COPYING |
| # net-misc/strongswan-5.0.2-r4: strongswan-5.0.2/COPYING |
| # strongswan-5.0.2/LICENSE |
| # sys-process/procps-3.2.8_p11: debian/copyright procps-3.2.8/COPYING |
| logging.info('License(s) for %s: %s', self.fullnamerev, |
| ' '.join(license_files)) |
| for license_file in sorted(license_files): |
| # Joy and pink ponies. Some license_files are encoded as latin1 while |
| # others are utf-8 and of course you can't know but only guess. |
| license_path = os.path.join(workdir, license_file) |
| license_txt = ReadUnknownEncodedFile(license_path, "Adding License") |
| |
| self.license_text_scanned += [ |
| "Scanned Source License %s:\n\n%s" % (license_file, license_txt)] |
| |
| # We used to clean up here, but there have been many instances where |
| # looking at unpacked source to see where the licenses were, was useful |
| # so let's disable this for now |
| # self._RunEbuildPhases(['clean']) |
| |
| def GetPackageInfo(self, fullnamewithrev): |
| """Populate PackageInfo with package license, and homepage. |
| |
| self.ebuild_license_names will not be filled if the package is skipped |
| or if there was an issue getting data from the ebuild. |
| self.license_names will only get the licenses that we can paste |
| as shared licenses. |
| scan_source_for_licenses will be set if we should unpack the source to look |
| for licenses |
| if need_copyright_attribution is also set, not finding a license in the |
| source is fatal (PackageLicenseError will get raised). |
| |
| Args: |
| fullnamewithrev: e.g. dev-libs/libatomic_ops-7.2d |
| |
| Raises: |
| AssertionError: on runtime errors |
| """ |
| if not fullnamewithrev: |
| if not self.build_source_tree: |
| raise AssertionError("Cannot continue without full name or source tree") |
| fullnamewithrev = "%s/%s" % (self._BuildInfo("CATEGORY"), |
| self._BuildInfo("PF")) |
| logging.debug("Computed package name %s from %s", fullnamewithrev, |
| self.build_source_tree) |
| |
| try: |
| cpv = portage_utilities.SplitCPV(fullnamewithrev) |
| # A bad package can either raise a TypeError exception or return None, |
| # so we catch both cases. |
| if not cpv: |
| raise TypeError |
| except TypeError: |
| raise AssertionError("portage couldn't find %s, missing version number?" % |
| fullnamewithrev) |
| |
| (self.category, self.name, self.version, self.revision) = ( |
| cpv.category, cpv.package, cpv.version_no_rev, cpv.rev) |
| |
| if self.revision is not None: |
| self.revision = str(self.revision).lstrip('r') |
| if self.revision == '0': |
| self.revision = None |
| |
| if self.category in SKIPPED_CATEGORIES: |
| logging.info("%s in SKIPPED_CATEGORIES, skip package", self.fullname) |
| self.skip = True |
| return |
| |
| if self.fullname in SKIPPED_PACKAGES: |
| logging.info("%s in SKIPPED_PACKAGES, skip package", self.fullname) |
| self.skip = True |
| return |
| |
| def _ReadEbuildInfo(self): |
| """Populate package info from an ebuild retrieved via equery.""" |
| # By default, equery returns the latest version of the package. A |
| # build may have used an older version than what is currently |
| # available in the source tree (a build dependency can be pinned |
| # to an older version of a package for compatibility |
| # reasons). Therefore we need to tell equery that we want the |
| # exact version number used in the image build as opposed to the |
| # latest available in the source tree. |
| args = ['equery-%s' % self.board, 'which', self.fullnamerev] |
| try: |
| path = cros_build_lib.RunCommand(args, print_cmd=debug, |
| redirect_stdout=True).output.strip() |
| if not path: |
| raise AssertionError |
| except: |
| raise AssertionError('GetEbuildPath for %s failed.\n' |
| 'Is your tree clean? Delete %s and rebuild' % |
| (self.name, |
| cros_build_lib.GetSysroot(board=self.board))) |
| logging.debug("%s -> %s", " ".join(args), path) |
| |
| if not os.access(path, os.F_OK): |
| raise AssertionError("Can't access %s", path) |
| |
| self.ebuild_path = path |
| |
| args = ['portageq-%s' % self.board, 'metadata', |
| cros_build_lib.GetSysroot(board=self.board), 'ebuild', |
| self.fullnamerev, 'HOMEPAGE', 'LICENSE'] |
| tmp = cros_build_lib.RunCommand(args, print_cmd=debug, |
| redirect_stdout=True) |
| lines = tmp.output.splitlines() |
| # Runs: |
| # portageq metadata /build/x86-alex ebuild net-misc/wget-1.12-r2 \ |
| # HOMEPAGE LICENSE |
| # Returns: |
| # http://www.gnu.org/software/wget/ |
| # GPL-3 |
| (self.homepages, self.ebuild_license_names) = ( |
| lines[0].split(), lines[1].split()) |
| |
| def GetLicenses(self): |
| """Get licenses from the ebuild field and the unpacked source code. |
| |
| Some packages have static license mappings applied to them that get |
| retrieved from the ebuild. |
| |
| For others, we figure out whether the package source should be scanned to |
| add licenses found there. |
| |
| Raises: |
| AssertionError: on runtime errors |
| PackageLicenseError: couldn't find license in ebuild and source. |
| """ |
| if self.build_source_tree: |
| self.homepages = self._BuildInfo("HOMEPAGE").split() |
| self.ebuild_license_names = self._BuildInfo("LICENSE").split() |
| else: |
| self._ReadEbuildInfo() |
| |
| if self.fullname in PACKAGE_HOMEPAGES: |
| self.homepages = PACKAGE_HOMEPAGES[self.fullname] |
| |
| # Packages with missing licenses or licenses that need mapping (like |
| # BSD/MIT) are hardcoded here: |
| if self.fullname in PACKAGE_LICENSES: |
| self.ebuild_license_names = PACKAGE_LICENSES[self.fullname] |
| logging.info("Static license mapping for %s: %s", self.fullnamerev, |
| ",".join(self.ebuild_license_names)) |
| else: |
| logging.info("Read licenses for %s: %s", self.fullnamerev, |
| ",".join(self.ebuild_license_names)) |
| |
| # Lots of packages in chromeos-base have their license set to BSD instead |
| # of BSD-Google: |
| new_license_names = [] |
| for license_name in self.ebuild_license_names: |
| # TODO: temp workaround for http;//crbug.com/348750 , remove when the bug |
| # is fixed. |
| if (license_name == "BSD" and |
| self.fullnamerev.startswith("chromeos-base/")): |
| license_name = "BSD-Google" |
| logging.error( |
| "Fixed BSD->BSD-Google for %s because it's in chromeos-base. " |
| "Please fix the LICENSE field in the ebuild", self.fullnamerev) |
| # TODO: temp workaround for http;//crbug.com/348749 , remove when the bug |
| # is fixed. |
| if license_name == "Proprietary": |
| license_name = "Google-TOS" |
| logging.error( |
| "Fixed Proprietary -> Google-TOS for %s. " |
| "Please fix the LICENSE field in the ebuild", self.fullnamerev) |
| new_license_names.append(license_name) |
| self.ebuild_license_names = new_license_names |
| |
| # The ebuild license field can look like: |
| # LICENSE="GPL-3 LGPL-3 Apache-2.0" (this means AND, as in all 3) |
| # for third_party/portage-stable/app-admin/rsyslog/rsyslog-5.8.11.ebuild |
| # LICENSE="|| ( LGPL-2.1 MPL-1.1 )" |
| # for third_party/portage-stable/x11-libs/cairo/cairo-1.8.8.ebuild |
| |
| # The parser isn't very smart and only has basic support for the |
| # || ( X Y ) OR logic to do the following: |
| # In order to save time needlessly unpacking packages and looking or a |
| # cleartext license (which is really a crapshoot), if we have a license |
| # like BSD that requires looking for copyright attribution, but we can |
| # chose another license like GPL, we do that. |
| |
| if not self.ebuild_license_names: |
| logging.error("%s: no license found in ebuild. FIXME!", self.fullnamerev) |
| # In a bind, you could comment this out. I'm making the output fail to |
| # get your attention since this error really should be fixed, but if you |
| # comment out the next line, the script will try to find a license inside |
| # the source. |
| raise PackageLicenseError() |
| |
| # This is not invalid, but the parser can't deal with it, so if it ever |
| # happens, error out to tell the programmer to do something. |
| # dev-python/pycairo-1.10.0-r4: LGPL-3 || ( LGPL-2.1 MPL-1.1 ) |
| if "||" in self.ebuild_license_names[1:]: |
| logging.error("%s: Can't parse || in the middle of a license: %s", |
| self.fullnamerev, ' '.join(self.ebuild_license_names)) |
| raise PackageLicenseError() |
| |
| or_licenses_and_one_is_no_attribution = False |
| # We do a quick early pass first so that the longer pass below can |
| # run accordingly. |
| for license_name in [x for x in self.ebuild_license_names |
| if x not in LICENCES_IGNORE]: |
| # Here we have an OR case, and one license that we can use stock, so |
| # we remember that in order to be able to skip license attributions if |
| # any were in the OR. |
| if (self.ebuild_license_names[0] == "||" and |
| license_name not in COPYRIGHT_ATTRIBUTION_LICENSES): |
| or_licenses_and_one_is_no_attribution = True |
| |
| for license_name in [x for x in self.ebuild_license_names |
| if x not in LICENCES_IGNORE]: |
| # Licenses like BSD or MIT can't be used as is because they do not contain |
| # copyright self. They have to be replaced by copyright file given in the |
| # source code, or manually mapped by us in PACKAGE_LICENSES |
| if license_name in COPYRIGHT_ATTRIBUTION_LICENSES: |
| # To limit needless efforts, if a package is BSD or GPL, we ignore BSD |
| # and use GPL to avoid scanning the package, but we can only do this if |
| # or_licenses_and_one_is_no_attribution has been set above. |
| # This ensures that if we have License: || (BSD3 BSD4), we will |
| # look in the source. |
| if or_licenses_and_one_is_no_attribution: |
| logging.info("%s: ignore license %s because ebuild LICENSES had %s", |
| self.fullnamerev, license_name, |
| ' '.join(self.ebuild_license_names)) |
| else: |
| logging.info("%s: can't use %s, will scan source code for copyright", |
| self.fullnamerev, license_name) |
| self.need_copyright_attribution = True |
| self.scan_source_for_licenses = True |
| else: |
| self.license_names.add(license_name) |
| # We can't display just 2+ because it only contains text that says to |
| # read v2 or v3. |
| if license_name == 'GPL-2+': |
| self.license_names.add('GPL-2') |
| if license_name == 'LGPL-2+': |
| self.license_names.add('LGPL-2') |
| |
| if license_name in LOOK_IN_SOURCE_LICENSES: |
| logging.info("%s: Got %s, will try to find better license in source...", |
| self.fullnamerev, license_name) |
| self.scan_source_for_licenses = True |
| |
| if self.license_names: |
| logging.info('%s: using stock|cust license(s) %s', |
| self.fullnamerev, ','.join(self.license_names)) |
| |
| # If the license(s) could not be found, or one requires copyright |
| # attribution, dig in the source code for license files: |
| # For instance: |
| # Read licenses from ebuild for net-dialup/ppp-2.4.5-r3: BSD,GPL-2 |
| # We need get the substitution file for BSD and add it to GPL. |
| if self.scan_source_for_licenses: |
| self._ExtractLicenses() |
| |
| # This shouldn't run, but leaving as sanity check. |
| if not self.license_names and not self.license_text_scanned: |
| raise AssertionError("Didn't find usable licenses for %s" % |
| self.fullnamerev) |
| |
| |
| class Licensing(object): |
| """Do the actual work of extracting licensing info and outputting html.""" |
| |
| def __init__(self, board, package_fullnames, gen_licenses): |
| # eg x86-alex |
| self.board = board |
| # List of stock and custom licenses referenced in ebuilds. Used to |
| # print a report. Dict value says which packages use that license. |
| self.licenses = {} |
| |
| # Licenses are supposed to be generated at package build time and be |
| # ready for us, but in case they're not, they can be generated. |
| self.gen_licenses = gen_licenses |
| |
| # This keeps track of whether we have an incomplete license file due to |
| # package errors during parsing. |
| # Any non empty list at the end shows the list of packages that caused |
| # errors. |
| self.incomplete_packages = [] |
| |
| self.package_text = {} |
| self.entry_template = None |
| |
| # We need to have a dict for the list of packages objects, index by package |
| # fullnamerev, so that when we scan our licenses at the end, and find out |
| # some shared licenses are only used by one package, we can access that |
| # package object by name, and add the license directly in that object. |
| self.packages = {} |
| self._package_fullnames = package_fullnames |
| |
| @property |
| def sorted_licenses(self): |
| return sorted(self.licenses.keys(), key=str.lower) |
| |
| def _SaveLicenseDump(self, pkg): |
| if pkg.build_source_tree: |
| save_file = "%s/build-info/license.yaml" % pkg.build_source_tree |
| else: |
| save_file = pkg.license_dump_path |
| logging.debug("Saving license to %s", save_file) |
| save_dir = os.path.dirname(save_file) |
| if not os.path.isdir(save_dir): |
| os.makedirs(save_dir, 0755) |
| with open(save_file, "w") as f: |
| yaml_dump = [] |
| for key, value in pkg.__dict__.items(): |
| yaml_dump.append([key, value]) |
| f.write(yaml.dump(yaml_dump)) |
| |
| def _LoadLicenseDump(self, pkg): |
| save_file = pkg.license_dump_path |
| logging.debug("Getting license from %s for %s", save_file, pkg.name) |
| with open(save_file, "r") as f: |
| # yaml.safe_load barfs on unicode it output, but we don't really need it. |
| yaml_dump = yaml.load(f) |
| for key, value in yaml_dump: |
| pkg.__dict__[key] = value |
| |
| def LicensedPackages(self, license_name): |
| """Return list of packages using a given license.""" |
| return self.licenses[license_name] |
| |
| def LoadPackageInfo(self, board): |
| """Populate basic package info for all packages from their ebuild.""" |
| for package_name in self._package_fullnames: |
| pkg = PackageInfo() |
| pkg.board = board |
| pkg.GetPackageInfo(package_name) |
| self.packages[package_name] = pkg |
| |
| def HookPackageProcess(self, pkg_build_path): |
| """Different entry point to populate a packageinfo. |
| |
| This is called instead of LoadPackageInfo when called by a package build. |
| |
| Args: |
| pkg_build_path: unpacked being built by emerge. |
| """ |
| pkg = PackageInfo() |
| pkg.build_source_tree = pkg_build_path |
| pkg.GetPackageInfo(None) |
| if not pkg.skip: |
| pkg.GetLicenses() |
| self._SaveLicenseDump(pkg) |
| |
| def ProcessPackageLicenses(self): |
| """Iterate through all packages provided and gather their licenses. |
| |
| GetLicenses will scrape licenses from the code and/or gather stock license |
| names. We gather the list of stock and custom ones for later processing. |
| |
| Do not call this after adding virtual packages with AddExtraPkg. |
| """ |
| for package_name in self.packages: |
| pkg = self.packages[package_name] |
| if pkg.skip: |
| if self.gen_licenses: |
| logging.info("Package %s is in skip list", package_name) |
| else: |
| # If we do a licensing run expecting to get licensing objects from |
| # an image build, virtual packages will be missing such objects |
| # because virtual packages do not get the install hook run at build |
| # time. Because this script may not have permissions to write in the |
| # /var/db/ directory, we don't want it to generate useless license |
| # bits for virtual packages. As a result, ignore virtual packages |
| # here. |
| if pkg.category == "virtual": |
| logging.debug("Ignoring %s virtual package", package_name) |
| continue |
| |
| # Other skipped packages get dumped with incomplete info and the skip flag |
| if not os.path.exists(pkg.license_dump_path) and not self.gen_licenses: |
| logging.warning(">>> License for %s is missing, creating now <<<", |
| package_name) |
| if not os.path.exists(pkg.license_dump_path) or self.gen_licenses: |
| if not pkg.skip: |
| try: |
| pkg.GetLicenses() |
| except PackageLicenseError: |
| pkg.licensing_failed = True |
| # We dump packages where licensing failed too. |
| self._SaveLicenseDump(pkg) |
| |
| # To debug the code, we force the data to be re-read from the dumps |
| # instead of reusing what we may have in memory. |
| for package_name in self.packages: |
| pkg = self.packages[package_name] |
| if pkg.category == "virtual": |
| continue |
| |
| self._LoadLicenseDump(pkg) |
| logging.debug("loaded dump for %s", pkg.fullnamerev) |
| if pkg.skip: |
| logging.info("Package %s is in skip list", pkg.fullnamerev) |
| if pkg.licensing_failed: |
| logging.info("Package %s failed licensing", pkg.fullnamerev) |
| self.incomplete_packages += [pkg.fullnamerev] |
| |
| def AddExtraPkg(self, pkg_data): |
| """Allow adding pre-created virtual packages. |
| |
| GetLicenses will not work on them, so add them after having run |
| ProcessPackages. |
| |
| Args: |
| pkg_data: array of package data as defined below |
| """ |
| pkg = PackageInfo() |
| pkg.board = self.board |
| pkg.category = pkg_data[0] |
| pkg.name = pkg_data[1] |
| pkg.version = pkg_data[2] |
| pkg.homepages = pkg_data[3] # this is a list |
| pkg.license_names = pkg_data[4] # this is also a list |
| pkg.ebuild_license_names = pkg_data[4] |
| self.packages[pkg.fullnamerev] = pkg |
| |
| # Called directly by src/repohooks/pre-upload.py |
| @staticmethod |
| def FindLicenseType(license_name): |
| """Says if a license is stock Gentoo, custom, or doesn't exist.""" |
| |
| for directory in STOCK_LICENSE_DIRS: |
| path = '%s/%s' % (directory, license_name) |
| if os.path.exists(path): |
| return "Gentoo Package Stock" |
| |
| for directory in CUSTOM_LICENSE_DIRS: |
| path = '%s/%s' % (directory, license_name) |
| if os.path.exists(path): |
| return "Custom" |
| |
| raise AssertionError(""" |
| license %s could not be found in %s |
| If the license in the ebuild is correct, |
| a) a stock license should be added to portage-stable/licenses : |
| running `cros_portage_upgrade` inside of the chroot should clone this repo |
| to /tmp/portage/: |
| https://chromium.googlesource.com/chromiumos/overlays/portage/+/gentoo |
| find the new licenses under licenses, and add them to portage-stable/licenses |
| |
| b) if it's a non gentoo package with a custom license, you can copy that license |
| to third_party/chromiumos-overlay/licenses/ |
| |
| Try re-running the script with -p cat/package-ver --generate |
| after fixing the license.""" % |
| (license_name, |
| '\n'.join(STOCK_LICENSE_DIRS + CUSTOM_LICENSE_DIRS)) |
| ) |
| |
| @staticmethod |
| def ReadSharedLicense(license_name): |
| """Read and return stock or cust license file specified in an ebuild.""" |
| |
| license_path = None |
| for directory in STOCK_LICENSE_DIRS + CUSTOM_LICENSE_DIRS: |
| path = os.path.join(directory, license_name) |
| if os.path.exists(path): |
| license_path = path |
| break |
| |
| if license_path: |
| return ReadUnknownEncodedFile(license_path, "read license") |
| else: |
| raise AssertionError("license %s could not be found in %s" |
| % (license_name, |
| '\n'.join(STOCK_LICENSE_DIRS + |
| CUSTOM_LICENSE_DIRS)) |
| ) |
| |
| @staticmethod |
| def EvaluateTemplate(template, env): |
| """Expand a template with vars like {{foo}} using a dict of expansions.""" |
| # TODO switch to stock python templates. |
| for key, val in env.iteritems(): |
| template = template.replace('{{%s}}' % key, val) |
| return template |
| |
| def _GeneratePackageLicenseText(self, pkg): |
| """Concatenate all licenses related to a pkg. |
| |
| This means a combination of ebuild shared licenses and licenses read from |
| the pkg source tree, if any. |
| |
| Args: |
| pkg: PackageInfo object |
| |
| Raises: |
| AssertionError: on runtime errors |
| """ |
| license_text = [] |
| for license_text_scanned in pkg.license_text_scanned: |
| license_text.append(license_text_scanned) |
| license_text.append('%s\n' % ('-=' * 40)) |
| |
| license_pointers = [] |
| # sln: shared license name. |
| for sln in pkg.license_names: |
| # Says whether it's a stock gentoo or custom license. |
| license_type = self.FindLicenseType(sln) |
| license_pointers.append( |
| "<li><a href='#%s'>%s License %s</a></li>" % ( |
| sln, license_type, sln)) |
| |
| # This should get caught earlier, but one extra check. |
| if not license_text + license_pointers: |
| raise AssertionError('Ended up with no license_text for %s', pkg.name) |
| |
| env = { |
| 'name': "%s-%s" % (pkg.name, pkg.version), |
| 'url': cgi.escape(pkg.homepages[0]) if pkg.homepages else '', |
| 'licenses_txt': cgi.escape('\n'.join(license_text)) or '', |
| 'licenses_ptr': '\n'.join(license_pointers) or '', |
| } |
| self.package_text[pkg] = self.EvaluateTemplate(self.entry_template, env) |
| |
| def GenerateHTMLLicenseOutput(self, output_file, |
| output_template=TMPL, |
| entry_template=ENTRY_TMPL, |
| license_template=SHARED_LICENSE_TMPL): |
| """Generate the combined html license file used in ChromeOS. |
| |
| Args: |
| output_file: resulting HTML license output. |
| output_template: template for the entire HTML file. |
| entry_template: template for per package entries. |
| license_template: template for shared license entries. |
| """ |
| self.entry_template = ReadUnknownEncodedFile(entry_template) |
| sorted_license_txt = [] |
| |
| # Keep track of which licenses are used by which packages. |
| for pkg in self.packages.values(): |
| if pkg.skip or pkg.licensing_failed: |
| continue |
| for sln in pkg.license_names: |
| self.licenses.setdefault(sln, []).append(pkg.fullnamerev) |
| |
| # Find licenses only used once, and roll them in the package that uses them. |
| # We use keys() because licenses is modified in the loop, so we can't use |
| # an iterator. |
| for sln in self.licenses.keys(): |
| if len(self.licenses[sln]) == 1: |
| pkg_fullnamerev = self.licenses[sln][0] |
| logging.info("Collapsing shared license %s into single use license " |
| "(only used by %s)", sln, pkg_fullnamerev) |
| license_type = self.FindLicenseType(sln) |
| license_txt = self.ReadSharedLicense(sln) |
| single_license = "%s License %s:\n\n%s" % (license_type, sln, |
| license_txt) |
| pkg = self.packages[pkg_fullnamerev] |
| pkg.license_text_scanned.append(single_license) |
| pkg.license_names.remove(sln) |
| del self.licenses[sln] |
| |
| for pkg in sorted(self.packages.values(), |
| key=lambda x: (x.name.lower(), x.version, x.revision)): |
| if pkg.skip: |
| logging.debug("Skipping package %s", pkg.fullnamerev) |
| continue |
| if pkg.licensing_failed: |
| logging.debug("Package %s failed licensing, skipping", pkg.fullnamerev) |
| continue |
| self._GeneratePackageLicenseText(pkg) |
| sorted_license_txt += [self.package_text[pkg]] |
| |
| # Now generate the bottom of the page that will contain all the shared |
| # licenses and a list of who is pointing to them. |
| license_template = ReadUnknownEncodedFile(license_template) |
| |
| licenses_txt = [] |
| for license_name in self.sorted_licenses: |
| env = { |
| 'license_name': license_name, |
| 'license': cgi.escape(self.ReadSharedLicense(license_name)), |
| 'license_type': self.FindLicenseType(license_name), |
| 'license_packages': ' '.join(self.LicensedPackages(license_name)), |
| } |
| licenses_txt += [self.EvaluateTemplate(license_template, env)] |
| |
| file_template = ReadUnknownEncodedFile(output_template) |
| env = { |
| 'entries': '\n'.join(sorted_license_txt), |
| 'licenses': '\n'.join(licenses_txt), |
| } |
| osutils.WriteFile(output_file, |
| self.EvaluateTemplate(file_template, env).encode('UTF-8')) |
| |
| |
| def ListInstalledPackages(board, all_packages=False): |
| """Return a list of all packages installed for a particular board.""" |
| |
| # If all_packages is set to True, all packages visible in the build |
| # chroot are used to generate the licensing file. This is not what you want |
| # for a release license file, but it's a way to run licensing checks against |
| # all packages. |
| # If it's set to False, it will only generate a licensing file that contains |
| # packages used for a release build (as determined by the dependencies for |
| # virtual/target-os). |
| |
| if all_packages: |
| # The following returns all packages that were part of the build tree |
| # (many get built or used during the build, but do not get shipped). |
| # Note that it also contains packages that are in the build as |
| # defined by build_packages but not part of the image we ship. |
| args = ["equery-%s" % board, "list", "*"] |
| packages = cros_build_lib.RunCommand(args, print_cmd=debug, |
| redirect_stdout=True |
| ).output.splitlines() |
| else: |
| # The following returns all packages that were part of the build tree |
| # (many get built or used during the build, but do not get shipped). |
| # Note that it also contains packages that are in the build as |
| # defined by build_packages but not part of the image we ship. |
| args = ["emerge-%s" % board, "--with-bdeps=y", "--usepkgonly", |
| "--emptytree", "--pretend", "--color=n", "virtual/target-os"] |
| emerge = cros_build_lib.RunCommand(args, print_cmd=debug, |
| redirect_stdout=True).output.splitlines() |
| # Another option which we've decided not to use, is bdeps=n. This outputs |
| # just the packages we ship, but does not packages that were used to build |
| # them, including a package like flex which generates a .a that is included |
| # and shipped in ChromeOS. |
| # We've decided to credit build packages, even if we're not legally required |
| # to (it's always nice to do), and that way we get corner case packages like |
| # flex. This is why we use bdep=y and not bdep=n. |
| |
| packages = [] |
| # [binary R ] x11-libs/libva-1.1.1 to /build/x86-alex/ |
| pkg_rgx = re.compile(r'\[[^]]+R[^]]+\] (.+) to /build/.*') |
| # If we match something else without the 'R' like |
| # [binary U ] chromeos-base/pepper-flash-13.0.0.133-r1 [12.0.0.77-r1] |
| # this is bad and we should die on this. |
| pkg_rgx2 = re.compile(r'(\[[^]]+\] .+) to /build/.*') |
| for line in emerge: |
| match = pkg_rgx.search(line) |
| match2 = pkg_rgx2.search(line) |
| if match: |
| packages.append(match.group(1)) |
| elif match2: |
| raise AssertionError("Package incorrectly installed, try eclean-%s" % |
| board, "\n%s" % match2.group(1)) |
| |
| return packages |
| |
| |
| def _HandleIllegalXMLChars(text): |
| """Handles illegal XML Characters. |
| |
| XML 1.0 acceptable character range: |
| Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | \ |
| [#x10000-#x10FFFF] |
| |
| This function finds all illegal characters in the text and filters |
| out all whitelisted characters (e.g. ^L). |
| |
| Args: |
| text: text to examine. |
| |
| Returns: |
| Filtered |text| and a list of non-whitelisted illegal characters found. |
| """ |
| whitelist_re = re.compile(u'[\x0c]') |
| text = whitelist_re.sub('', text) |
| # illegal_chars_re includes all illegal characters (whitelisted or |
| # not), so we can expand the whitelist without modifying this line. |
| illegal_chars_re = re.compile( |
| u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]') |
| return (text, illegal_chars_re.findall(text)) |
| |
| |
| def ReadUnknownEncodedFile(file_path, logging_text=None): |
| """Read a file of unknown encoding (UTF-8 or latin) by trying in sequence. |
| |
| Args: |
| file_path: what to read. |
| logging_text: what to display for logging depending on file read. |
| |
| Returns: |
| File content, possibly converted from latin1 to UTF-8. |
| |
| Raises: |
| Assertion error: if non-whitelisted illegal XML characters |
| are found in the file. |
| ValueError: returned if we get invalid XML. |
| """ |
| try: |
| with codecs.open(file_path, encoding="utf-8") as c: |
| file_txt = c.read() |
| if logging_text: |
| logging.info("%s %s (UTF-8)", logging_text, file_path) |
| except UnicodeDecodeError: |
| with codecs.open(file_path, encoding="latin1") as c: |
| file_txt = c.read() |
| if logging_text: |
| logging.info("%s %s (latin1)", logging_text, file_path) |
| |
| file_txt, char_list = _HandleIllegalXMLChars(file_txt) |
| |
| if char_list: |
| raise ValueError('Illegal XML characters %s found in %s.' % |
| (char_list, file_path)) |
| |
| return file_txt |
| |
| |
| def NonHookMain(opts): |
| """Do the work when we're not called as a hook.""" |
| |
| board, all_packages, gen_licenses, output_file = ( |
| opts.board, opts.all_packages, opts.gen_licenses, opts.output) |
| packages_mode = bool(opts.package) |
| |
| if not board: |
| raise AssertionError("No board given (--board)") |
| logging.info("Using board %s.", board) |
| |
| builddir = os.path.join(cros_build_lib.GetSysroot(board=board), |
| 'tmp', 'portage') |
| if not os.path.exists(builddir): |
| raise AssertionError( |
| "FATAL: %s missing.\n" |
| "Did you give the right board and build that tree?" % builddir) |
| if not output_file and not gen_licenses: |
| logging.warning("You are not generating licenses and you didn't ask for " |
| "output. As a result this script will do nothing useful.") |
| license_dir = "%s/%s/" % (cros_build_lib.GetSysroot(board), |
| PER_PKG_LICENSE_DIR) |
| if not os.path.exists(license_dir): |
| raise AssertionError("FATAL: %s missing.\n" % license_dir) |
| |
| if gen_licenses and os.geteuid() != 0: |
| raise AssertionError("Run with sudo if you use --generate-licenses.") |
| |
| if packages_mode: |
| packages = opts.package |
| else: |
| packages = ListInstalledPackages(board, all_packages) |
| if not packages: |
| raise AssertionError('FATAL: Could not get any packages for board %s' % |
| board) |
| logging.debug("Initial Package list to work through:\n%s", |
| '\n'.join(sorted(packages))) |
| licensing = Licensing(board, packages, gen_licenses) |
| licensing.LoadPackageInfo(board) |
| logging.debug("Package list to skip:\n%s", |
| '\n'.join([p for p in sorted(packages) |
| if licensing.packages[p].skip])) |
| logging.debug("Package list left to work through:\n%s", |
| '\n'.join([p for p in sorted(packages) |
| if not licensing.packages[p].skip])) |
| licensing.ProcessPackageLicenses() |
| if not packages_mode: |
| # We add 2 virtual packages as well as 2 boot packages that are included |
| # with some hardware, but not in the image or package list. |
| for extra_pkg in [ |
| ['x11-base', 'X.Org', '1.9.3', ['http://www.x.org/'], ['X']], |
| ['sys-kernel', 'Linux', '2.6', ['http://www.kernel.org/'], ['GPL-2']], |
| ['sys-boot', 'u-boot', '2013.06', ['http://www.denx.de/wiki/U-Boot'], |
| ['GPL-2+']], |
| ['sys-boot', 'coreboot', '2013.04', ['http://www.coreboot.org/'], |
| ['GPL-2']], |
| ]: |
| licensing.AddExtraPkg(extra_pkg) |
| |
| if output_file: |
| licensing.GenerateHTMLLicenseOutput(output_file) |
| |
| if licensing.incomplete_packages: |
| raise AssertionError(""" |
| DO NOT USE OUTPUT!!! |
| Some packages are missing due to errors, please look at errors generated |
| during this run. |
| List of packages with errors: |
| %s |
| """ % '\n'.join(licensing.incomplete_packages)) |
| |
| |
| def main(args): |
| # pylint: disable=W0603 |
| global debug |
| # pylint: enable=W0603 |
| |
| parser = commandline.ArgumentParser(usage=__doc__) |
| parser.add_argument("-b", "--board", |
| help="which board to run for, like x86-alex") |
| parser.add_argument("-p", "--package", action="append", default=[], |
| help="check the license of the package, e.g.," |
| "dev-libs/libatomic_ops-7.2d") |
| parser.add_argument("-a", "--all-packages", action="store_true", |
| dest="all_packages", |
| help="Run licensing against all packages in the " |
| "build tree") |
| parser.add_argument("-g", "--generate-licenses", action="store_true", |
| dest="gen_licenses", |
| help="Generate licensing bits for each package before " |
| "making license file\n(default is to use build time " |
| "license bits)") |
| parser.add_argument("-k", "--hook", type="path", dest="hook", |
| help="Hook mode takes a single package and outputs its " |
| "license on stdout. Give $PORTAGE_BUILDDIR as argument.") |
| parser.add_argument("-o", "--output", type="path", |
| help="which html file to create with output") |
| opts = parser.parse_args(args) |
| debug = opts.debug |
| debug = True |
| hook_path = opts.hook |
| |
| |
| # This get called from src/scripts/hooks/install/gen-package-licenses.sh |
| if hook_path: |
| licensing = Licensing(None, None, True) |
| licensing.HookPackageProcess(hook_path) |
| else: |
| NonHookMain(opts) |