Merge lp:~jmwilson/duplicity/filecaps into lp:~duplicity-team/duplicity/0.7-series

Proposed by James Wilson
Status: Work in progress
Proposed branch: lp:~jmwilson/duplicity/filecaps
Merge into: lp:~duplicity-team/duplicity/0.7-series
Diff against target: 309 lines (+140/-28)
7 files modified
bin/duplicity (+3/-7)
debian/control (+3/-0)
debian/duplicity.docs (+2/-0)
debian/duplicity.postinst (+6/-0)
debian/rules (+6/-4)
duplicity/selection.py (+7/-13)
setup.py (+113/-4)
To merge this branch: bzr merge lp:~jmwilson/duplicity/filecaps
Reviewer Review Type Date Requested Status
duplicity-team Pending
Review via email: mp+266773@code.launchpad.net

This proposal supersedes a proposal from 2015-04-27.

Description of the change

Continuing the earlier discussion on using file capabilities, this will use cx_Freeze to create a binary executable for duplicity, and if the setcap command is available during package installation, then it will set inherited capabilities cap_chown,cap_dac_read_search,cap_fowner on the executable. This means if the running user has inherited these capabilities (typically through a pam module), then when they run duplicity, they will be able to read any file, and use chown and chmod on any file. No grant of privileges applies for users who do not have these capabilities in their inherited set.

I've been running these changes to do full system backups on my machine for weeks without running as root, and I also used it to restore my home directory in an emergency. I installed libpam-cap, and then added a line "cap_dac_read_search backup" in /etc/security/capability.conf. In /etc/crontab, I run duplicity as the backup user, and it inherits the permissions needed to backup everything.

Most of the changes are in setup.py to run cx_Freeze as part of the build step. I'll highlight some of the code changes to duplicity with their rationale:

bin/duplicity: Use execvpe instead of execve since argv[0] is not guaranteed to be a full path. It seems the python interpreter makes argv[0] a full path, at least on some systems. The setuid(geteuid()) incantation doesn't really accomplish anything if geteuid() == 0.

duplicity/selection.py: os.access() won't take into account either EUID or capabilities: "The check is done using the calling process's real UID and GID, rather than the effective IDs as is done when actually attempting an operation (e.g., open(2)) on the file." If duplicity is run as a regular user with cap_dac_read_search effective permissions, it can read any file, but os.access may return false. The preferred way to see if you can read from a file is to attempt to open it for reading, so instead we just catch the exception later on when we try to use the path.

To post a comment you must log in.
Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote : Posted in a previous version of this proposal
Download full text (7.6 KiB)

If I understand this correctly, then new files will be created by
'duplicity', but older files owned by root will not be deletable.
Correct? I'm thinking a one-time 'chown -R duplicity: ~/cache/duplicity/*'
would fix that problem.

On Sun, Apr 26, 2015 at 7:31 PM, James Wilson <email address hidden> wrote:

> James Wilson has proposed merging lp:~jmwilson/duplicity/capabilities into
> lp:duplicity.
>
> Requested reviews:
> duplicity-team (duplicity-team)
>
> For more details, see:
> https://code.launchpad.net/~jmwilson/duplicity/capabilities/+merge/257488
>
> Proposal is to add an unprivileged "duplicity" user during installation
> that is used to limit the capabilities when duplicity is run as root. To
> manage capabilities, I'm using the python bindings for libcap-ng (which
> needs its own updating since at least the current vivid package is empty
> for some reason, but is correct when built from source).
>
> I've been interested in using duplicity for system-wide backups, but one
> thing that is troubling is that it must be run as root. As an interpreted
> program that communicates on the network, this exposes the host to possible
> bugs in python or any other packages imported in duplicity.
>
> First, examining the code in bin/duplicity:
> # if python is run setuid, it's only partway set,
> # so make sure to run with euid/egid of root
> if os.geteuid() == 0:
> # make sure uid/gid match euid/egid
> os.setuid(os.geteuid())
> os.setgid(os.getegid())
>
> I'm not sure what this is for; if you're root (i.e., os.geteuid() == 0)
> then there's no need to switch the real uid. When the machination of
> "setuid(geteuid())" is used it is typically when the ruid=0 and we want to
> irrevocably drop privileges to a euid!=0 normal user. That's not the case
> here, so this doesn't help or harm us.
>
> Then there's the issue of running duplicity as root. Since it's an
> interpreted program, the normal ways of expanding privileges (SUID
> executable or setcap on the script) are unavailable, and the other options
> are to run it as root, or put SUID or file capabilities on the python
> interpreter.
>
> The only reason to run duplicity as root is get read access to the whole
> file system. We can safely drop all capabilities other than
> CAP_DAC_READ_SEARCH. Since we're still root, we'll get all capabilities
> back if we do execve, so we could also change the bounding set or lock the
> securebits to prevent the kernel from re-granting privileges on execve.
> Nonetheless, we're still root, and lots of important files are owned by and
> writable to root, so the best choice is to change uid to an unprivileged
> user who maintains only the ability to read the whole file system.
>
> It's hard to test the change directly, since it doesn't change the output
> or actions of duplicity. The following python script mimics what the code
> does and demonstrates the reduction of capabilities:
>
> #!/usr/bin/env python
>
> from __future__ import print_function
> from capng import *
> import fcntl
> import os
> import pwd
> import signal
> import sys
>
> if os.geteuid() != 0:
> sys.exit()
>
> user = pwd.getpwnam("nobody")
> capng_clear...

Read more...

Revision history for this message
edso (ed.so) wrote : Posted in a previous version of this proposal
Download full text (8.0 KiB)

looks like capng is difficult to retrieve. if so i would hesitate to make duplicity depend on it by default.

does it work with older python? we officially support python 2.6+ still.

..ede/duply.net

On 27.04.2015 15:15, Kenneth Loafman wrote:
> If I understand this correctly, then new files will be created by
> 'duplicity', but older files owned by root will not be deletable.
> Correct? I'm thinking a one-time 'chown -R duplicity: ~/cache/duplicity/*'
> would fix that problem.
>
>
> On Sun, Apr 26, 2015 at 7:31 PM, James Wilson <email address hidden> wrote:
>
>> James Wilson has proposed merging lp:~jmwilson/duplicity/capabilities into
>> lp:duplicity.
>>
>> Requested reviews:
>> duplicity-team (duplicity-team)
>>
>> For more details, see:
>> https://code.launchpad.net/~jmwilson/duplicity/capabilities/+merge/257488
>>
>> Proposal is to add an unprivileged "duplicity" user during installation
>> that is used to limit the capabilities when duplicity is run as root. To
>> manage capabilities, I'm using the python bindings for libcap-ng (which
>> needs its own updating since at least the current vivid package is empty
>> for some reason, but is correct when built from source).
>>
>> I've been interested in using duplicity for system-wide backups, but one
>> thing that is troubling is that it must be run as root. As an interpreted
>> program that communicates on the network, this exposes the host to possible
>> bugs in python or any other packages imported in duplicity.
>>
>> First, examining the code in bin/duplicity:
>> # if python is run setuid, it's only partway set,
>> # so make sure to run with euid/egid of root
>> if os.geteuid() == 0:
>> # make sure uid/gid match euid/egid
>> os.setuid(os.geteuid())
>> os.setgid(os.getegid())
>>
>> I'm not sure what this is for; if you're root (i.e., os.geteuid() == 0)
>> then there's no need to switch the real uid. When the machination of
>> "setuid(geteuid())" is used it is typically when the ruid=0 and we want to
>> irrevocably drop privileges to a euid!=0 normal user. That's not the case
>> here, so this doesn't help or harm us.
>>
>> Then there's the issue of running duplicity as root. Since it's an
>> interpreted program, the normal ways of expanding privileges (SUID
>> executable or setcap on the script) are unavailable, and the other options
>> are to run it as root, or put SUID or file capabilities on the python
>> interpreter.
>>
>> The only reason to run duplicity as root is get read access to the whole
>> file system. We can safely drop all capabilities other than
>> CAP_DAC_READ_SEARCH. Since we're still root, we'll get all capabilities
>> back if we do execve, so we could also change the bounding set or lock the
>> securebits to prevent the kernel from re-granting privileges on execve.
>> Nonetheless, we're still root, and lots of important files are owned by and
>> writable to root, so the best choice is to change uid to an unprivileged
>> user who maintains only the ability to read the whole file system.
>>
>> It's hard to test the change directly, since it doesn't change the output
>> or actions of duplicity. The following python script mimics what t...

Read more...

Revision history for this message
Michael Terry (mterry) wrote : Posted in a previous version of this proposal

Also, this should fallback gracefully if the 'duplicity' user doesn't exist.

Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote : Posted in a previous version of this proposal
Download full text (8.7 KiB)

I cannot find python-capng for Ubuntu 14.04, Python 2.7, so I'm guessing
it's a new critter.

On Mon, Apr 27, 2015 at 8:33 AM, edso <email address hidden> wrote:

> looks like capng is difficult to retrieve. if so i would hesitate to make
> duplicity depend on it by default.
>
> does it work with older python? we officially support python 2.6+ still.
>
> ..ede/duply.net
>
> On 27.04.2015 15:15, Kenneth Loafman wrote:
> > If I understand this correctly, then new files will be created by
> > 'duplicity', but older files owned by root will not be deletable.
> > Correct? I'm thinking a one-time 'chown -R duplicity:
> ~/cache/duplicity/*'
> > would fix that problem.
> >
> >
> > On Sun, Apr 26, 2015 at 7:31 PM, James Wilson <email address hidden> wrote:
> >
> >> James Wilson has proposed merging lp:~jmwilson/duplicity/capabilities
> into
> >> lp:duplicity.
> >>
> >> Requested reviews:
> >> duplicity-team (duplicity-team)
> >>
> >> For more details, see:
> >>
> https://code.launchpad.net/~jmwilson/duplicity/capabilities/+merge/257488
> >>
> >> Proposal is to add an unprivileged "duplicity" user during installation
> >> that is used to limit the capabilities when duplicity is run as root. To
> >> manage capabilities, I'm using the python bindings for libcap-ng (which
> >> needs its own updating since at least the current vivid package is empty
> >> for some reason, but is correct when built from source).
> >>
> >> I've been interested in using duplicity for system-wide backups, but one
> >> thing that is troubling is that it must be run as root. As an
> interpreted
> >> program that communicates on the network, this exposes the host to
> possible
> >> bugs in python or any other packages imported in duplicity.
> >>
> >> First, examining the code in bin/duplicity:
> >> # if python is run setuid, it's only partway set,
> >> # so make sure to run with euid/egid of root
> >> if os.geteuid() == 0:
> >> # make sure uid/gid match euid/egid
> >> os.setuid(os.geteuid())
> >> os.setgid(os.getegid())
> >>
> >> I'm not sure what this is for; if you're root (i.e., os.geteuid() == 0)
> >> then there's no need to switch the real uid. When the machination of
> >> "setuid(geteuid())" is used it is typically when the ruid=0 and we want
> to
> >> irrevocably drop privileges to a euid!=0 normal user. That's not the
> case
> >> here, so this doesn't help or harm us.
> >>
> >> Then there's the issue of running duplicity as root. Since it's an
> >> interpreted program, the normal ways of expanding privileges (SUID
> >> executable or setcap on the script) are unavailable, and the other
> options
> >> are to run it as root, or put SUID or file capabilities on the python
> >> interpreter.
> >>
> >> The only reason to run duplicity as root is get read access to the whole
> >> file system. We can safely drop all capabilities other than
> >> CAP_DAC_READ_SEARCH. Since we're still root, we'll get all capabilities
> >> back if we do execve, so we could also change the bounding set or lock
> the
> >> securebits to prevent the kernel from re-granting privileges on execve.
> >> Nonetheless, we're still root, and lots of important files are owned by
...

Read more...

Revision history for this message
James Wilson (jmwilson) wrote : Posted in a previous version of this proposal

Oops, the package is python-cap-ng. The problem is that the distro version is missing the goods. I think they forgot to include a dependency in their control file, since if I grab the source it builds fine and I didn't have to do anything. It's tracked in https://bugs.launchpad.net/ubuntu/+source/libcap-ng/+bug/1244384 and yeah it looks like they've been sitting on it for quite a while.

Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote : Posted in a previous version of this proposal

I marked this as a "Work in Progress" because I don't want to forget it, but it sounds like it would be a support nightmare without distro support or pip support. Perhaps someone will put together a PPA with the appropriate packaging, but for now, we'll just have to wait.

Revision history for this message
James Wilson (jmwilson) wrote : Posted in a previous version of this proposal

Hey, I wanted to resume this discussion. Since April a couple of things have changed: 1) I helped the maintainer of libcap-ng to fix its build issues as of 0.7.4-3 that is available in sid and stretch, and 2) I found another way to limit root capabilities using file capabilities that has a smaller footprint (it doesn't create a new system user).

The catch for #2 is that file capabilities need a real binary executable, because the kernel ignores file capabilities set on interpreted programs. I found a utility called cx_Freeze that uses a minimal binary loader to start up the python environment and run a bundled script. Then we can set inherited file capabilities on /usr/bin/duplicity in the post-install script. The inherited capabilities cannot be harnessed unless the executing user also has the same capabilities in their inherited set, which the system administrator can take care of by granting inherited capabilities on the backup user. The easiest way to do this is using pam_cap.so. One possible issue is that cx_Freeze is available as an ubuntu package (cx-freeze) but it is not in Debian.

I've been running a custom version of duplicity with this setup for several weeks. It's a cleaner solution since it doesn't require a specific duplicity user to exist. The tradeoff is that system-wide backups require more setup since the system administrator will need to set up the inherited user capabilities. It's worth adding some documentation on how to do all of this.

Are either of these directions something you'd like to consider now?

Revision history for this message
Kenneth Loafman (kenneth-loafman) wrote : Posted in a previous version of this proposal

I've run cx-freeze as well and I like it (gsutil on Freebsd). From the
standpoint of isolation it would give us the ability to supply all versions
of all needed modules as well as the Python version itself (for those that
refuse to upgrade). The cost is that the package becomes huge, but it's
just tradeoff, our support time vs a bit of their storage space.

What we might consider doing is both a frozen and a non-frozen version.
The users still on Python 2.4 (gack!) could upgrade to the latest and
greatest by using the frozen version, while the more advanced could upgrade
normally. I know it works on all Linux distros and Freebsd.

As to file capabilities, that will depend on what kernel version they are
running and whether it's baked into the filesystem. I suppose the code
could check for availability, use if possible and operate as normal if not.

On Fri, Jun 5, 2015 at 12:27 PM, James Wilson <email address hidden> wrote:

> Hey, I wanted to resume this discussion. Since April a couple of things
> have changed: 1) I helped the maintainer of libcap-ng to fix its build
> issues as of 0.7.4-3 that is available in sid and stretch, and 2) I found
> another way to limit root capabilities using file capabilities that has a
> smaller footprint (it doesn't create a new system user).
>
> The catch for #2 is that file capabilities need a real binary executable,
> because the kernel ignores file capabilities set on interpreted programs. I
> found a utility called cx_Freeze that uses a minimal binary loader to start
> up the python environment and run a bundled script. Then we can set
> inherited file capabilities on /usr/bin/duplicity in the post-install
> script. The inherited capabilities cannot be harnessed unless the executing
> user also has the same capabilities in their inherited set, which the
> system administrator can take care of by granting inherited capabilities on
> the backup user. The easiest way to do this is using pam_cap.so. One
> possible issue is that cx_Freeze is available as an ubuntu package
> (cx-freeze) but it is not in Debian.
>
> I've been running a custom version of duplicity with this setup for
> several weeks. It's a cleaner solution since it doesn't require a specific
> duplicity user to exist. The tradeoff is that system-wide backups require
> more setup since the system administrator will need to set up the inherited
> user capabilities. It's worth adding some documentation on how to do all of
> this.
>
> Are either of these directions something you'd like to consider now?
> --
> https://code.launchpad.net/~jmwilson/duplicity/capabilities/+merge/257488
> You are subscribed to branch lp:duplicity.
>

Revision history for this message
Aaron Whitehouse (aaron-whitehouse) :

Unmerged revisions

1114. By James Wilson

Use file capabilities on frozen executable

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/duplicity'
2--- bin/duplicity 2015-05-08 12:28:47 +0000
3+++ bin/duplicity 2015-08-03 18:32:04 +0000
4@@ -1302,7 +1302,7 @@
5 log.Notice(_("RESTART: The first volume failed to upload before termination.\n"
6 " Restart is impossible...starting backup from beginning."))
7 self.last_backup.delete()
8- os.execve(sys.argv[0], sys.argv, os.environ)
9+ os.execvpe(sys.argv[0], sys.argv, os.environ)
10 elif mf_len - self.start_vol > 0:
11 # upload of N vols failed, fix manifest and restart
12 log.Notice(_("RESTART: Volumes %d to %d failed to upload before termination.\n"
13@@ -1317,7 +1317,7 @@
14 " backup then restart the backup from the beginning.") %
15 (mf_len, self.start_vol))
16 self.last_backup.delete()
17- os.execve(sys.argv[0], sys.argv, os.environ)
18+ os.execvpe(sys.argv[0], sys.argv, os.environ)
19
20 def setLastSaved(self, mf):
21 vi = mf.volume_info_dict[self.start_vol]
22@@ -1341,12 +1341,8 @@
23 See https://bugs.launchpad.net/duplicity/+bug/931175
24 """), log.ErrorCode.pythonoptimize_set)
25
26- # if python is run setuid, it's only partway set,
27- # so make sure to run with euid/egid of root
28 if os.geteuid() == 0:
29- # make sure uid/gid match euid/egid
30- os.setuid(os.geteuid())
31- os.setgid(os.getegid())
32+ log.Warn(_("Running duplicity as root is not recommend."))
33
34 # set the current time strings (make it available for command line processing)
35 dup_time.setcurtime()
36
37=== modified file 'debian/control'
38--- debian/control 2014-10-27 14:15:52 +0000
39+++ debian/control 2015-08-03 18:32:04 +0000
40@@ -15,6 +15,7 @@
41 python-pexpect,
42 rdiff,
43 rsync,
44+ cx-freeze,
45 Homepage: https://launchpad.net/duplicity
46 Standards-Version: 3.9.5
47 X-Python-Version: >= 2.6
48@@ -27,9 +28,11 @@
49 gnupg,
50 python-lockfile,
51 python-pexpect,
52+Recommends: libcap2-bin,
53 Suggests: ncftp,
54 python-boto,
55 python-paramiko,
56+ libpam-cap,
57 Description: encrypted bandwidth-efficient backup
58 Duplicity backs directories by producing encrypted tar-format volumes
59 and uploading them to a remote or local file server. Because duplicity
60
61=== added file 'debian/duplicity.docs'
62--- debian/duplicity.docs 1970-01-01 00:00:00 +0000
63+++ debian/duplicity.docs 2015-08-03 18:32:04 +0000
64@@ -0,0 +1,2 @@
65+README
66+README-LOG
67
68=== added file 'debian/duplicity.postinst'
69--- debian/duplicity.postinst 1970-01-01 00:00:00 +0000
70+++ debian/duplicity.postinst 2015-08-03 18:32:04 +0000
71@@ -0,0 +1,6 @@
72+#!/bin/sh -e
73+
74+PROGRAM=/usr/bin/duplicity
75+
76+[ -e "${PROGRAM}" -a -x "${PROGRAM}" ] || exit
77+which setcap >/dev/null && setcap -q cap_chown,cap_dac_read_search,cap_fowner=ei "${PROGRAM}"
78
79=== modified file 'debian/rules'
80--- debian/rules 2014-10-31 20:33:27 +0000
81+++ debian/rules 2015-08-03 18:32:04 +0000
82@@ -12,14 +12,16 @@
83
84 override_dh_auto_install:
85 dh_auto_install
86-
87+
88 # Debian installs docs itself in /usr/share/doc/duplicity/
89 rm -r debian/duplicity/usr/share/doc/duplicity-*
90-
91+
92 # Modify upstream's version string into the right version
93 find debian/duplicity -name "*\$$version*" | xargs rename "s/\\\$$version/$(UPSTREAM_VERSION)/g"
94 find debian/duplicity -name "*_version*" | xargs rename "s/_version/$(UPSTREAM_VERSION)/g"
95 grep -Rl "\$$version" debian/duplicity | xargs sed -i "s/\$$version/$(UPSTREAM_VERSION)/g"
96
97-override_dh_installdocs:
98- dh_installdocs README README-LOG
99+override_dh_strip:
100+ # Don't strip the executable since it will remove the attached zip
101+ # archive, and the base image from cx_Freeze is already stripped.
102+ dh_strip --exclude=/usr/bin/duplicity
103
104=== modified file 'duplicity/selection.py'
105--- duplicity/selection.py 2015-07-31 08:22:31 +0000
106+++ duplicity/selection.py 2015-08-03 18:32:04 +0000
107@@ -151,16 +151,7 @@
108 for filename in robust.listpath(path):
109 new_path = robust.check_common_error(
110 error_handler, Path.append, (path, filename))
111- # make sure file is read accessible
112- if (new_path and new_path.type in ["reg", "dir"]
113- and not os.access(new_path.name, os.R_OK)):
114- log.Warn(_("Error accessing possibly locked file %s") % util.ufn(new_path.name),
115- log.WarningCode.cannot_read,
116- util.escape(new_path.name))
117- if diffdir.stats:
118- diffdir.stats.Errors += 1
119- new_path = None
120- elif new_path:
121+ if new_path:
122 s = self.Select(new_path)
123 if s == 1:
124 yield (new_path, 0)
125@@ -447,9 +438,12 @@
126
127 def exclude_sel_func(path):
128 # do not follow symbolic links when checking for file existence!
129- if path.isdir() and path.append(filename).exists():
130- return 0
131- else:
132+ try:
133+ if path.isdir() and path.append(filename).exists():
134+ return 0
135+ else:
136+ return None
137+ except OSError:
138 return None
139
140 if include == 0:
141
142=== modified file 'setup.py'
143--- setup.py 2015-02-01 17:37:37 +0000
144+++ setup.py 2015-08-03 18:32:04 +0000
145@@ -22,10 +22,15 @@
146
147 import sys
148 import os
149-from setuptools import setup, Extension
150+import cx_Freeze
151+from setuptools import setup, Command, Extension
152+from setuptools import Distribution as _Distribution
153 from setuptools.command.test import test
154 from setuptools.command.install import install
155 from setuptools.command.sdist import sdist
156+from distutils.command.clean import clean
157+import distutils.dir_util
158+import distutils.log
159
160 version_string = "$version"
161
162@@ -77,6 +82,9 @@
163 # And make sure our scripts are ready
164 build_scripts_cmd = self.get_finalized_command("build_scripts")
165 build_scripts_cmd.run()
166+ # And make sure our executables are ready
167+ build_exe_cmd = self.get_finalized_command("build_exe")
168+ build_exe_cmd.run()
169
170 # make symlinks for test data
171 if build_cmd.build_lib != top_dir:
172@@ -88,14 +96,45 @@
173 except Exception:
174 pass
175
176- os.environ['PATH'] = "%s:%s" % (
177+ os.environ['PATH'] = "%s:%s:%s" % (
178+ os.path.abspath(build_exe_cmd.build_exe),
179 os.path.abspath(build_scripts_cmd.build_dir),
180 os.environ.get('PATH'))
181
182 test.run(self)
183
184
185+class Distribution(_Distribution):
186+ def __init__(self, attrs):
187+ self.executables = []
188+ _Distribution.__init__(self, attrs)
189+
190+
191 class InstallCommand(install):
192+ user_options = install.user_options + [
193+ ('install-exe=', None, 'installation directory for executables'),
194+ ]
195+
196+ def expand_dirs(self):
197+ install.expand_dirs(self)
198+ self._expand_attrs(['install_exe'])
199+
200+ def get_sub_commands(self):
201+ subCommands = install.get_sub_commands(self)
202+ if self.distribution.executables:
203+ subCommands.append("install_exe")
204+ return [s for s in subCommands if s != "install_egg_info"]
205+
206+ def initialize_options(self):
207+ install.initialize_options(self)
208+ self.install_exe = None
209+
210+ def finalize_options(self):
211+ install.finalize_options(self)
212+ self.convert_paths('exe')
213+ if self.root is not None:
214+ self.change_roots('exe')
215+
216 def run(self):
217 # Normally, install will call build(). But we want to delete the
218 # testing dir between building and installing. So we manually build
219@@ -110,6 +149,67 @@
220
221 install.run(self)
222
223+ def select_scheme(self, name):
224+ install.select_scheme(self, name)
225+ if self.install_exe is None:
226+ self.install_exe = self.install_scripts
227+
228+
229+class InstallExeCommand(Command):
230+ description = "install executables built from Python scripts"
231+ user_options = [
232+ ('install-dir=', 'd', 'directory to install executables to'),
233+ ('build-dir=', 'b', 'build directory (where to install from)'),
234+ ('force', 'f', 'force installation (overwrite existing files)'),
235+ ('skip-build', None, 'skip the build steps'),
236+ ]
237+
238+ def initialize_options(self):
239+ self.install_dir = None
240+ self.force = 0
241+ self.build_dir = None
242+ self.skip_build = None
243+
244+ def finalize_options(self):
245+ self.set_undefined_options('build', ('build_exe', 'build_dir'))
246+ self.set_undefined_options('install',
247+ ('install_exe', 'install_dir'))
248+ self.set_undefined_options('install',
249+ ('force', 'force'),
250+ ('skip_build', 'skip_build'))
251+
252+ def run(self):
253+ if not self.skip_build:
254+ self.run_command('build_exe')
255+ self.outfiles = self.copy_tree(self.build_dir, self.install_dir)
256+
257+ def get_inputs(self):
258+ return self.distribution.executables or []
259+
260+ def get_outputs(self):
261+ return self.outfiles or []
262+
263+
264+class CleanCommand(clean):
265+ user_options = clean.user_options + [
266+ ('build-exe=', None, 'build directory for executables'),
267+ ]
268+
269+ def initialize_options(self):
270+ clean.initialize_options(self)
271+ self.build_exe = None
272+
273+ def finalize_options(self):
274+ clean.finalize_options(self)
275+ self.set_undefined_options('build_exe', ('build_exe', 'build_exe'))
276+
277+ def run(self):
278+ if os.path.exists(self.build_exe):
279+ distutils.dir_util.remove_tree(self.build_exe, dry_run=self.dry_run)
280+ else:
281+ distutils.log.warn("'%s' does not exist -- can't clean it", self.build_exe)
282+ clean.run(self)
283+
284
285 # TODO: move logic from dist/makedist inline
286 class SDistCommand(sdist):
287@@ -145,11 +245,20 @@
288 include_dirs=incdir_list,
289 library_dirs=libdir_list,
290 libraries=["rsync"])],
291- scripts=['bin/rdiffdir', 'bin/duplicity'],
292+ scripts=['bin/rdiffdir'],
293 data_files=data_files,
294 tests_require=['lockfile', 'mock', 'pexpect'],
295 test_suite='testing',
296 cmdclass={'test': TestCommand,
297 'install': InstallCommand,
298- 'sdist': SDistCommand},
299+ 'install_exe': InstallExeCommand,
300+ 'sdist': SDistCommand,
301+ 'build': cx_Freeze.build,
302+ 'build_exe': cx_Freeze.build_exe,
303+ 'clean': CleanCommand},
304+ distclass=Distribution,
305+ executables=[cx_Freeze.Executable('bin/duplicity')],
306+ options={"build_exe": {"copy_dependent_files": False,
307+ "create_shared_zip": False,
308+ "append_script_to_exe": True}},
309 )

Subscribers

People subscribed via source and target branches