diff --git a/doc/Makefile.in b/doc/Makefile.in index ed2d1a01c3..fada9c5c62 100644 --- a/doc/Makefile.in +++ b/doc/Makefile.in @@ -48,6 +48,7 @@ MAN1= \ port-info.1 \ port-install.1 \ port-installed.1 \ + port-licensecheck.1 \ port-lint.1 \ port-list.1 \ port-livecheck.1 \ diff --git a/doc/port-licensecheck.1 b/doc/port-licensecheck.1 new file mode 100644 index 0000000000..58194b6d17 --- /dev/null +++ b/doc/port-licensecheck.1 @@ -0,0 +1,107 @@ +'\" t +.TH "PORT\-LICENSECHECK" "1" "2\&.6\&.99" "MacPorts 2\&.6\&.99" "MacPorts Manual" +.\" ----------------------------------------------------------------- +.\" * Define some portability stuff +.\" ----------------------------------------------------------------- +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.\" http://bugs.debian.org/507673 +.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +port-licensecheck \- Check whether a port is disbributable +.SH "SYNOPSIS" +.sp +.nf +\fBport\fR [\fB\-vd\fR] [\fB\-D\fR \fIportdir\fR] \fBlicensecheck\fR + [[\fIportname\fR | \fIpseudo\-portname\fR | \fIport\-expressions\fR | \fIport\-url\fR]] +.fi +.SH "DESCRIPTION" +.sp +\fBport licensecheck\fR checks whether a port is distributable, meaning that not only does the license allow distribution, but so do its dependencies, and none of those dependencies cause license conflicts\&. +.sp +By default, it is assumed that ports may use libraries or headers from their dependencies and thus form a derivative work\&. A dependency with an incompatible license thus prevents the port from being distributed in binary form\&. If a dependency with an incompatible license is not used in such a way that a derivative work is formed, or should not prevent binary distribution for any other reason, list it in license_noconflict, see \fBportfile\fR(7)\&. +.sp +As an example, assume a port, A, is licensed under the GNU General Public License and depends on B, which is licensed under a BSD license\&. Now, if B depends on OpenSSL, we have a conflict, as the GPL conflicts with its license\&. In this case, we \fIcould\fR list port B in license_noconflict, but this requires careful inspection, and at the very least a comment in the Portfile\&. The most common case where it is safe to mark a license as non\-conflicting are: +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +If a port only a invokes a dependency as a build tool, it is most likely safe to list\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +If a port invokes a dependency as a tool, i\&.e\&. by executing its binaries, it may be safe to list\&. +.RE +.sp +.RS 4 +.ie n \{\ +\h'-04'\(bu\h'+03'\c +.\} +.el \{\ +.sp -1 +.IP \(bu 2.3 +.\} +If a port links against a dependency using dynamic linking or some form of higher\-level object oriented environment, it is probably not safe to list\&. +.RE +.sp +The above points should only be taken as examples and general guidelines; any addition of license_noconflict should probably go through a review on GitHub to ensure that the change is safe\&. +.sp +The MacPorts infrastructure only offers binary archives that pass this test\&. +.SH "GLOBAL OPTIONS" +.sp +Please see the section \fBGLOBAL OPTIONS\fR in the \fBport\fR(1) man page for a description of global port options\&. +.PP +\fB\-v\fR +.RS 4 +Print a message if the port is up to date\&. +.RE +.PP +\fB\-d\fR +.RS 4 +Print debugging information such as the regular expression and all matches\&. +.RE +.SH "SEE ALSO" +.sp +\fBport\fR(1), \fBportfile\fR(7), \fBport-upgrade\fR(7), \m[blue]\fBUsing Binaries in the MacPorts Guide\fR\m[]\&\s-2\u[1]\d\s+2 +.SH "AUTHORS" +.sp +.if n \{\ +.RS 4 +.\} +.nf +(C) 2014 The MacPorts Project +Clemens Lang +.fi +.if n \{\ +.RE +.\} +.SH "EXTERNAL REFERENCES" +.IP " 1." 4 +Using Binaries in the MacPorts Guide +.RS 4 +\%https://guide.macports.org/#using.binaries +.RE diff --git a/doc/port-licensecheck.1.txt b/doc/port-licensecheck.1.txt new file mode 100644 index 0000000000..1ec556b4cf --- /dev/null +++ b/doc/port-licensecheck.1.txt @@ -0,0 +1,69 @@ +// vim: set et sw=4 ts=8 ft=asciidoc tw=80: +port-licensecheck(1) +==================== + +NAME +---- +port-licensecheck - Check whether a port is disbributable + +SYNOPSIS +-------- +[cmdsynopsis] +*port* [*-vd*] [*-D* 'portdir'] *licensecheck* + [['portname' | 'pseudo-portname' | 'port-expressions' | 'port-url']] + +DESCRIPTION +----------- + +*port licensecheck* checks whether a port is distributable, meaning +that not only does the license allow distribution, but so do its +dependencies, and none of those dependencies cause license conflicts. + +By default, it is assumed that ports may use libraries or headers from +their dependencies and thus form a derivative work. A dependency with +an incompatible license thus prevents the port from being distributed +in binary form. If a dependency with an incompatible license is not +used in such a way that a derivative work is formed, or should not +prevent binary distribution for any other reason, list it in ++license_noconflict+, see man:portfile[7]. + +As an example, assume a port, A, is licensed under the GNU General +Public License and depends on B, which is licensed under a BSD +license. Now, if B depends on OpenSSL, we have a conflict, as the GPL +conflicts with its license. In this case, we _could_ list port B in ++license_noconflict+, but this requires careful inspection, and at the +very least a comment in the `Portfile`. The most common case where it +is safe to mark a license as non-conflicting are: + +* If a port only a invokes a dependency as a build tool, it is most + likely safe to list. +* If a port invokes a dependency as a tool, i.e. by executing its + binaries, it may be safe to list. +* If a port links against a dependency using dynamic linking or some + form of higher-level object oriented environment, it is probably not + safe to list. + +The above points should only be taken as examples and general +guidelines; any addition of `license_noconflict` should probably go +through a review on GitHub to ensure that the change is safe. + +The MacPorts infrastructure only offers binary archives that pass this +test. + +include::global-flags.txt[] + +*-v*:: + Print a message if the port is up to date. + +*-d*:: + Print debugging information such as the regular expression and all matches. + +SEE ALSO +-------- +man:port[1], man:portfile[7], man:port-upgrade[7], +guide:using.binaries[Using Binaries in the MacPorts Guide] + +AUTHORS +------- + (C) 2014 The MacPorts Project + Clemens Lang diff --git a/src/port/port.tcl b/src/port/port.tcl index ced83d301c..d4fbaab70e 100755 --- a/src/port/port.tcl +++ b/src/port/port.tcl @@ -4352,6 +4352,7 @@ array set action_array [list \ install [list action_target [ACTION_ARGS_PORTS]] \ clean [list action_target [ACTION_ARGS_PORTS]] \ test [list action_target [ACTION_ARGS_PORTS]] \ + licensecheck [list action_target [ACTION_ARGS_PORTS]] \ lint [list action_target [ACTION_ARGS_PORTS]] \ livecheck [list action_target [ACTION_ARGS_PORTS]] \ distcheck [list action_target [ACTION_ARGS_PORTS]] \ diff --git a/src/port1.0/Makefile.in b/src/port1.0/Makefile.in index f840d98967..095be5cdf3 100644 --- a/src/port1.0/Makefile.in +++ b/src/port1.0/Makefile.in @@ -12,7 +12,7 @@ SRCS= port.tcl portchecksum.tcl portconfigure.tcl portextract.tcl \ portlint.tcl portclean.tcl porttest.tcl portactivate.tcl portbump.tcl \ portdeactivate.tcl portstartupitem.tcl porttrace.tcl portlivecheck.tcl \ portdistcheck.tcl portmirror.tcl portload.tcl portunload.tcl portreload.tcl \ - portdistfiles.tcl fetch_common.tcl portsandbox.tcl + portdistfiles.tcl fetch_common.tcl portsandbox.tcl portlicensecheck.tcl include $(srcdir)/../../Mk/macports.subdir.mk diff --git a/src/port1.0/port.tcl b/src/port1.0/port.tcl index 1ec2de33c7..214ca1c4b5 100644 --- a/src/port1.0/port.tcl +++ b/src/port1.0/port.tcl @@ -78,6 +78,7 @@ package require portactivate 1.0 package require portdeactivate 1.0 package require portclean 1.0 package require porttest 1.0 +package require portlicensecheck 1.0 package require portlint 1.0 package require porttrace 1.0 package require portdistcheck 1.0 diff --git a/src/port1.0/portlicensecheck.tcl b/src/port1.0/portlicensecheck.tcl new file mode 100644 index 0000000000..8c6e462bba --- /dev/null +++ b/src/port1.0/portlicensecheck.tcl @@ -0,0 +1,336 @@ +# -*- coding: utf-8; mode: tcl; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:filetype=tcl:et:sw=4:ts=4:sts=4 +# +# Copyright (c) 2007 - 2016 The MacPorts Project +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of The MacPorts Project nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +package provide portlicensecheck 1.0 +package require portutil 1.0 + +set org.macports.licensecheck [target_new org.macports.licensecheck portlicensecheck::licensecheck_main] +target_runtype ${org.macports.licensecheck} always +target_state ${org.macports.licensecheck} no +target_provides ${org.macports.licensecheck} licensecheck +target_requires ${org.macports.licensecheck} main +target_prerun ${org.macports.licensecheck} portlicensecheck::licensecheck_start + +namespace eval portlicensecheck { +} + +set_ui_prefix + +set check_deptypes [list depends_build depends_lib] + +# Notes: +# 'Restrictive/Distributable' means a non-free license that nonetheless allows +# distributing binaries. +# 'Restrictive' means a non-free license that does not allow distributing +# binaries, and is thus not in the list. +# 'Permissive' is a catchall for other licenses that allow +# modification and distribution of source and binaries. +# 'Copyleft' means a license that requires source code to be made available, +# and derivative works to be licensed the same as the original. +# 'GPLConflict' should be added if the license conflicts with the GPL (and its +# variants like CeCILL and the AGPL) and is not in the list of licenses known +# to do so below. +# 'Noncommercial' means a license that prohibits commercial use. +set good_licenses [list afl agpl apache apsl artistic autoconf beopen bitstreamvera \ + boost bsd bsd-old cc-by cc-by-sa cddl cecill cecill-b cecill-c cnri copyleft \ + cpl curl epl fpll fontconfig freetype gd gfdl gpl \ + gplconflict ibmpl ijg isc jasper lgpl libtool lppl mit \ + mpl ncsa noncommercial openldap openssl permissive php \ + psf public-domain qpl restrictive/distributable ruby \ + sleepycat ssleay tcl/tk vim w3c wtfpl wxwidgets x11 zlib zpl] +foreach lic $good_licenses { + set license_good($lic) 1 +} + +proc portlicensecheck::all_licenses_except { args } { + set remaining $::good_licenses + foreach arg $args { + set remaining [lsearch -inline -all -not -exact $remaining $arg] + } + return $remaining +} + +# keep these values sorted +array set license_conflicts \ + [list \ + afl [list agpl cecill gpl] \ + agpl [list afl apache-1 apache-1.1 apsl beopen bsd-old cc-by-1 cc-by-2 cc-by-2.5 cc-by-3 cc-by-sa cddl cecill cnri cpl epl gd gpl-1 gpl-2 gplconflict ibmpl lppl mpl noncommercial openssl php qpl restrictive/distributable ruby ssleay zpl-1] \ + agpl-1 [list apache freetype gpl-3 gpl-3+ lgpl-3 lgpl-3+] \ + apache [list agpl-1 cecill gpl-1 gpl-2] \ + apache-1 [list agpl gpl] \ + apache-1.1 [list agpl gpl] \ + apsl [list agpl cecill gpl] \ + beopen [list agpl cecill gpl] \ + bsd-old [list agpl cecill gpl] \ + cc-by-1 [list agpl cecill gpl] \ + cc-by-2 [list agpl cecill gpl] \ + cc-by-2.5 [list agpl cecill gpl] \ + cc-by-3 [list agpl cecill gpl] \ + cc-by-sa [list agpl cecill gpl] \ + cddl [list agpl cecill gpl] \ + cecill [list afl agpl apache apsl beopen bsd-old cc-by-1 cc-by-2 cc-by-2.5 cc-by-3 cc-by-sa cddl cnri cpl epl gd gplconflict ibmpl lppl mpl noncommercial openssl php qpl restrictive/distributable ruby ssleay zpl-1] \ + cnri [list agpl cecill gpl] \ + cpl [list agpl cecill gpl] \ + epl [list agpl cecill gpl] \ + freetype [list agpl-1 gpl-2] \ + gd [list agpl cecill gpl] \ + gpl [list afl apache-1 apache-1.1 apsl beopen bsd-old cc-by-1 cc-by-2 cc-by-2.5 cc-by-3 cc-by-sa cddl cnri cpl epl gd gplconflict ibmpl lppl mpl noncommercial openssl php qpl restrictive/distributable ruby ssleay zpl-1] \ + gpl-1 [list agpl apache gpl-3 gpl-3+ lgpl-3 lgpl-3+] \ + gpl-2 [list agpl apache freetype gpl-3 gpl-3+ lgpl-3 lgpl-3+] \ + gpl-3 [list agpl-1 gpl-1 gpl-2] \ + gpl-3+ [list agpl-1 gpl-1 gpl-2] \ + gplconflict [list agpl cecill gpl] \ + ibmpl [list agpl cecill gpl] \ + lgpl-3 [list agpl-1 gpl-1 gpl-2] \ + lgpl-3+ [list agpl-1 gpl-1 gpl-2] \ + lppl [list agpl cecill gpl] \ + mpl [list agpl cecill gpl] \ + noncommercial [list agpl cecill gpl] \ + openssl [list agpl cecill gpl] \ + opensslexception [portlicensecheck::all_licenses_except openssl ssleay] \ + php [list agpl cecill gpl] \ + qpl [list agpl cecill gpl] \ + restrictive/distributable [list agpl cecill gpl] \ + ruby [list agpl cecill gpl] \ + ssleay [list agpl cecill gpl] \ + zpl-1 [list agpl cecill gpl] \ + ] + +# return deps and license for given port +proc portlicensecheck::infoForPort {portName variantInfo} { + set portSearchResult [mport_lookup $portName] + if {[llength $portSearchResult] < 1} { + puts stderr "Warning: port \"$portName\" not found" + return {} + } + array set portInfo [lindex $portSearchResult 1] + set portfile_path [getportdir $portInfo(porturl)]/Portfile + set variant_string $variantInfo + + set dependencyList {} + set mport [mport_open $portInfo(porturl) [list subport $portInfo(name)] $variantInfo] + array unset portInfo + array set portInfo [mport_info $mport] + # Closing the mport is actually fairly expensive and not really necessary + mport_close $mport + + foreach dependencyType $::check_deptypes { + if {[info exists portInfo($dependencyType)] && $portInfo($dependencyType) ne ""} { + foreach dependency $portInfo($dependencyType) { + lappend dependencyList [string range $dependency [string last ":" $dependency]+1 end] + } + } + } + + set ret [list $dependencyList $portInfo(license)] + if {[info exists portInfo(installs_libs)]} { + lappend ret $portInfo(installs_libs) + } else { + # when in doubt, assume code from the dep is incorporated + lappend ret yes + } + if {[info exists portInfo(license_noconflict)]} { + lappend ret $portInfo(license_noconflict) + } + + return $ret +} + +# return license with any trailing dash followed by a number and/or plus sign removed +set remove_version_re {[0-9.+]+} +proc portlicensecheck::remove_version {license} { + set dash [string last - $license] + if {$dash != -1 && [regexp $::remove_version_re [string range $license $dash+1 end]]} { + return [string range $license 0 $dash-1] + } else { + return $license + } +} + +proc portlicensecheck::licensecheck_start {args} { + global UI_PREFIX subport + ui_notice "$UI_PREFIX [format [msgcat::mc "Checking license for %s"] ${subport}]" +} + +proc portlicensecheck::licensecheck_main {args} { + global UI_PREFIX subport portvariants PortInfo + + array set portSeen {} + set result 0 + + set top_info [infoForPort $subport $PortInfo(active_variants)] + if {$top_info eq {}} { + return 1 + } + set top_license [lindex $top_info 1] + foreach noconflict_port [lindex $top_info 3] { + set noconflict_ports($noconflict_port) 1 + } + set top_license_names {} + # check that top-level port's license(s) are good + foreach sublist $top_license { + # each element may be a list of alternatives (i.e. only one need apply) + set any_good 0 + set sub_names {} + foreach full_lic $sublist { + # chop off any trailing version number + set lic [remove_version $full_lic] + # add name to the list for later + lappend sub_names $lic + if {[info exists ::license_good([string tolower $lic])]} { + set any_good 1 + } + } + lappend top_license_names $sub_names + if {!$any_good} { + ui_notice "\"$subport\" is not distributable because its license \"$lic\" is not known to be distributable" + return 1 + } + } + + # start with deps of top-level port + set portPaths [dict create [lindex $top_info 0] [list]] + set portList [lindex $top_info 0] + foreach aPort $portList { + dict set portPaths $aPort [list] + } + + while {[llength $portList] > 0} { + set aPort [lindex $portList 0] + set portList [lreplace $portList 0 0] + if {[info exists portSeen($aPort)] && $portSeen($aPort) eq 1} { + continue + } + + # mark as seen and remove from the list + set portSeen($aPort) 1 + if {[info exists noconflict_ports($aPort)]} { + continue + } + + set aPortInfo [infoForPort $aPort $PortInfo(active_variants)] + if {$aPortInfo eq {}} { + continue + } + set aPortLicense [lindex $aPortInfo 1] + set installs_libs [lindex $aPortInfo 2] + if {!$installs_libs} { + continue + } + set parentPath [list {*}[dict get $portPaths $aPort] $aPort] + + ui_debug "checking $aPort" + + foreach sublist $aPortLicense { + set any_good 0 + set any_compatible 0 + # check that this dependency's license(s) are good + foreach full_lic $sublist { + set lic [remove_version [string tolower $full_lic]] + if {[info exists ::license_good($lic)]} { + set any_good 1 + } else { + # no good being compatible with other licenses if it's not distributable itself + continue + } + + # ... and that they don't conflict with the top-level port's + set any_conflict 0 + foreach top_sublist [concat $top_license $top_license_names] { + set any_sub_compatible 0 + foreach top_lic $top_sublist { + if {![info exists ::license_conflicts([string tolower $top_lic])] + || ([lsearch -sorted $::license_conflicts([string tolower $top_lic]) $lic] == -1 + && [lsearch -sorted $::license_conflicts([string tolower $top_lic]) [string tolower $full_lic]] == -1)} { + set any_sub_compatible 1 + break + } + } + if {!$any_sub_compatible} { + set any_conflict 1 + break + } + } + if {!$any_conflict} { + set any_compatible 1 + break + } + } + + if {!$any_good} { + ui_warn "\"$subport\" is not distributable because its dependency \"$aPort\" has license \"$full_lic\" which is not known to be distributable: [join $parentPath " -> "]" + set result 1 + } elseif {!$any_compatible} { + ui_warn "\"$subport\" is not distributable because its license \"$top_lic\" conflicts with license \"$full_lic\": [join $parentPath " -> "]" + set result 1 + } + } + + # skip deps that are explicitly stated to not conflict + array unset aPort_noconflict_ports + foreach noconflict_port [lindex $aPortInfo 3] { + set aPort_noconflict_ports($noconflict_port) 1 + } + # add its deps to the list + foreach possiblyNewPort [lindex $aPortInfo 0] { + if {![info exists portSeen($possiblyNewPort)] && ![info exists aPort_noconflict_ports($possiblyNewPort)]} { + lappend portList $possiblyNewPort + dict set portPaths $possiblyNewPort $parentPath + } + } + } + + if {$result eq 0} { + ui_info "\"$subport\" is distributable" + } + + return $result +} + +# given a variant string, return an array of variations +set split_variants_re {([-+])([[:alpha:]_]+[\w\.]*)} +proc portlicensecheck::split_variants {variants} { + set result {} + set l [regexp -all -inline -- $::split_variants_re $variants] + foreach { match sign variant } $l { + lappend result $variant $sign + } + return $result +} + +# given an array of variations, return a variant string in normalized form +proc portlicensecheck::normalize_variants {variations} { + array set varray $variations + set variant_string "" + foreach vname [lsort -ascii [array names varray]] { + append variant_string $varray($vname)${vname} + } + return $variant_string +}