From: Rob Browning Date: Sat, 12 Oct 2019 21:47:16 +0000 (-0500) Subject: Move bup to cmd/ and symlink ./bup to cmd/bup X-Git-Tag: 0.31~244 X-Git-Url: https://arthur.barton.de/cgi-bin/gitweb.cgi?p=bup.git;a=commitdiff_plain;h=3fa656946d28bb8cac061b745e2edc26bc2d56ae Move bup to cmd/ and symlink ./bup to cmd/bup Move main.py to cmd/bup and make ./bup a permanent symlink to it. Make the installed bup executable a relative symlink to the LIBDIR/cmd/bup file. Always use the location of the bup executable as a way to locate the other subcommands and the lib and resource directories, either when running from the source tree or from an install tree. This work finishes the switch to have all the commands to rely on the bup-python wrapper so that we can establish some norms we'll need to support Python 3. It should also allow us to simplify the main.py startup process. Signed-off-by: Rob Browning --- diff --git a/.gitignore b/.gitignore index d82e27a..8ea45fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -/bup /cmd/bup-* randomgen memtest diff --git a/Makefile b/Makefile index 338d595..d285f3a 100644 --- a/Makefile +++ b/Makefile @@ -50,22 +50,13 @@ bup_cmds := cmd/bup-python \ $(patsubst cmd/%-cmd.py,cmd/bup-%,$(wildcard cmd/*-cmd.py)) \ $(patsubst cmd/%-cmd.sh,cmd/bup-%,$(wildcard cmd/*-cmd.sh)) -bup_deps := bup lib/bup/_checkout.py lib/bup/_helpers$(SOEXT) $(bup_cmds) +bup_deps := lib/bup/_checkout.py lib/bup/_helpers$(SOEXT) $(bup_cmds) all: $(bup_deps) Documentation/all $(current_sampledata) -bup: - ln -s main.py bup - $(current_sampledata): t/configure-sampledata --setup -define install-python-bin - set -e; \ - sed -e '1 s|.*|#!$(bup_python)|; 2,/^# end of bup preamble$$/d' $1 > $2; \ - chmod 0755 $2; -endef - PANDOC ?= $(shell type -p pandoc) ifeq (,$(PANDOC)) @@ -98,8 +89,11 @@ install: all test -z "$(man_roff)" || $(INSTALL) -m 0644 $(man_roff) $(dest_mandir)/man1 test -z "$(man_html)" || install -d $(dest_docdir) test -z "$(man_html)" || $(INSTALL) -m 0644 $(man_html) $(dest_docdir) - $(call install-python-bin,bup,"$(dest_bindir)/bup") + $(INSTALL) -pm 0755 cmd/bup $(dest_libdir)/cmd/ $(INSTALL) -pm 0755 cmd/bup-* $(dest_libdir)/cmd/ + cd "$(dest_bindir)" && \ + ln -sf "$$($(bup_python) -c 'import os; print(os.path.relpath("$(abspath $(dest_libdir))/cmd/bup"))')" + set -e; \ $(INSTALL) -pm 0644 \ lib/bup/*.py \ $(dest_libdir)/bup @@ -321,7 +315,6 @@ clean: Documentation/clean cmd/bup-python rm -f *.o lib/*/*.o *.so lib/*/*.so *.dll lib/*/*.dll *.exe \ .*~ *~ */*~ lib/*/*~ lib/*/*/*~ \ *.pyc */*.pyc lib/*/*.pyc lib/*/*/*.pyc \ - bup \ randomgen memtest \ testfs.img lib/bup/t/testfs.img for x in $$(ls cmd/*-cmd.py cmd/*-cmd.sh | grep -vF python-cmd.sh | cut -b 5-); do \ diff --git a/bup b/bup new file mode 120000 index 0000000..f4083c1 --- /dev/null +++ b/bup @@ -0,0 +1 @@ +cmd/bup \ No newline at end of file diff --git a/cmd/bup b/cmd/bup new file mode 100755 index 0000000..e442541 --- /dev/null +++ b/cmd/bup @@ -0,0 +1,274 @@ +#!/bin/sh +"""": # -*-python-*- # -*-python-*- +set -e +top="$(pwd)" +cmdpath="$0" +# loop because macos doesn't have recursive readlink/realpath utils +while test -L "$cmdpath"; do + link="$(readlink "$cmdpath")" + cd "$(dirname "$cmdpath")" + cmdpath="$link" +done +script_home="$(cd "$(dirname "$cmdpath")" && pwd -P)" +cd "$top" +exec "$script_home/bup-python" "$0" ${1+"$@"} +""" +# end of bup preamble + +from __future__ import absolute_import, print_function +import errno, re, sys, os, subprocess, signal, getopt + +if sys.version_info[0] != 2 \ + and not os.environ.get('BUP_ALLOW_UNEXPECTED_PYTHON_VERSION') == 'true': + print('error: bup may crash with python versions other than 2, or eat your data', + file=sys.stderr) + sys.exit(2) + +from subprocess import PIPE +from sys import stderr, stdout +import select + +argv = sys.argv +exe = os.path.realpath(argv[0]) +exepath = os.path.split(exe)[0] or '.' + +# fix the PYTHONPATH to include our lib dir +if os.path.exists("%s/../bup/." % exepath): + # Everything is relative to exepath (i.e. LIBDIR/cmd/) + cmdpath = exepath + libpath = os.path.join(exepath, '..') + resourcepath = libpath +else: + # running from the src directory without being installed first + cmdpath = exepath + libpath = os.path.join(exepath, '../lib') + resourcepath = libpath +sys.path[:0] = [libpath] +os.environ['PYTHONPATH'] = libpath + ':' + os.environ.get('PYTHONPATH', '') +os.environ['BUP_MAIN_EXE'] = os.path.abspath(exe) +os.environ['BUP_RESOURCE_PATH'] = resourcepath + + +from bup import compat, helpers +from bup.compat import add_ex_tb, add_ex_ctx, wrap_main +from bup.helpers import atoi, columnate, debug1, log, merge_dict, tty_width + + +def usage(msg=""): + log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] ' + ' [options...]\n\n') + common = dict( + ftp = 'Browse backup sets using an ftp-like client', + fsck = 'Check backup sets for damage and add redundancy information', + fuse = 'Mount your backup sets as a filesystem', + help = 'Print detailed help for the given command', + index = 'Create or display the index of files to back up', + on = 'Backup a remote machine to the local one', + restore = 'Extract files from a backup set', + save = 'Save files into a backup set (note: run "bup index" first)', + tag = 'Tag commits for easier access', + web = 'Launch a web server to examine backup sets', + ) + + log('Common commands:\n') + for cmd,synopsis in sorted(common.items()): + log(' %-10s %s\n' % (cmd, synopsis)) + log('\n') + + log('Other available commands:\n') + cmds = [] + for c in sorted(os.listdir(cmdpath) + os.listdir(exepath)): + if c.startswith('bup-') and c.find('.') < 0: + cname = c[4:] + if cname not in common: + cmds.append(c[4:]) + log(columnate(cmds, ' ')) + log('\n') + + log("See 'bup help COMMAND' for more information on " + + "a specific command.\n") + if msg: + log("\n%s\n" % msg) + sys.exit(99) + + +if len(argv) < 2: + usage() + +# Handle global options. +try: + optspec = ['help', 'version', 'debug', 'profile', 'bup-dir='] + global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec) +except getopt.GetoptError as ex: + usage('error: %s' % ex.msg) + +help_requested = None +do_profile = False + +for opt in global_args: + if opt[0] in ['-?', '--help']: + help_requested = True + elif opt[0] in ['-V', '--version']: + subcmd = ['version'] + elif opt[0] in ['-D', '--debug']: + helpers.buglvl += 1 + os.environ['BUP_DEBUG'] = str(helpers.buglvl) + elif opt[0] in ['--profile']: + do_profile = True + elif opt[0] in ['-d', '--bup-dir']: + os.environ['BUP_DIR'] = opt[1] + else: + usage('error: unexpected option "%s"' % opt[0]) + +# Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.). +if 'BUP_DIR' in os.environ: + os.environ['BUP_DIR'] = os.path.abspath(os.environ['BUP_DIR']) + +if len(subcmd) == 0: + if help_requested: + subcmd = ['help'] + else: + usage() + +if help_requested and subcmd[0] != 'help': + subcmd = ['help'] + subcmd + +if len(subcmd) > 1 and subcmd[1] == '--help' and subcmd[0] != 'help': + subcmd = ['help', subcmd[0]] + subcmd[2:] + +subcmd_name = subcmd[0] +if not subcmd_name: + usage() + +def subpath(s): + sp = os.path.join(exepath, 'bup-%s' % s) + if not os.path.exists(sp): + sp = os.path.join(cmdpath, 'bup-%s' % s) + return sp + +subcmd[0] = subpath(subcmd_name) +if not os.path.exists(subcmd[0]): + usage('error: unknown command "%s"' % subcmd_name) + +already_fixed = atoi(os.environ.get('BUP_FORCE_TTY')) +if subcmd_name in ['mux', 'ftp', 'help']: + already_fixed = True +fix_stdout = not already_fixed and os.isatty(1) +fix_stderr = not already_fixed and os.isatty(2) + +if fix_stdout or fix_stderr: + tty_env = merge_dict(os.environ, + {'BUP_FORCE_TTY': str((fix_stdout and 1 or 0) + + (fix_stderr and 2 or 0))}) +else: + tty_env = os.environ + + +sep_rx = re.compile(br'([\r\n])') + +def print_clean_line(dest, content, width, sep=None): + """Write some or all of content, followed by sep, to the dest fd after + padding the content with enough spaces to fill the current + terminal width or truncating it to the terminal width if sep is a + carriage return.""" + global sep_rx + assert sep in (b'\r', b'\n', None) + if not content: + if sep: + os.write(dest, sep) + return + for x in content: + assert not sep_rx.match(x) + content = b''.join(content) + if sep == b'\r' and len(content) > width: + content = content[width:] + os.write(dest, content) + if len(content) < width: + os.write(dest, b' ' * (width - len(content))) + if sep: + os.write(dest, sep) + +def filter_output(src_out, src_err, dest_out, dest_err): + """Transfer data from src_out to dest_out and src_err to dest_err via + print_clean_line until src_out and src_err close.""" + global sep_rx + assert not isinstance(src_out, bool) + assert not isinstance(src_err, bool) + assert not isinstance(dest_out, bool) + assert not isinstance(dest_err, bool) + assert src_out is not None or src_err is not None + assert (src_out is None) == (dest_out is None) + assert (src_err is None) == (dest_err is None) + pending = {} + pending_ex = None + try: + fds = tuple([x for x in (src_out, src_err) if x is not None]) + while fds: + ready_fds, _, _ = select.select(fds, [], []) + width = tty_width() + for fd in ready_fds: + buf = os.read(fd, 4096) + dest = dest_out if fd == src_out else dest_err + if not buf: + fds = tuple([x for x in fds if x is not fd]) + print_clean_line(dest, pending.pop(fd, []), width) + else: + split = sep_rx.split(buf) + while len(split) > 1: + content, sep = split[:2] + split = split[2:] + print_clean_line(dest, + pending.pop(fd, []) + [content], + width, + sep) + assert(len(split) == 1) + if split[0]: + pending.setdefault(fd, []).extend(split) + except BaseException as ex: + pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex) + try: + # Try to finish each of the streams + for fd, pending_items in compat.items(pending): + dest = dest_out if fd == src_out else dest_err + try: + print_clean_line(dest, pending_items, width) + except (EnvironmentError, EOFError) as ex: + pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex) + except BaseException as ex: + pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex) + if pending_ex: + raise pending_ex + +def run_subcmd(subcmd): + + c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd + if not (fix_stdout or fix_stderr): + os.execvp(c[0], c) + + p = None + try: + p = subprocess.Popen(c, + stdout=PIPE if fix_stdout else sys.stdout, + stderr=PIPE if fix_stderr else sys.stderr, + env=tty_env, bufsize=4096, close_fds=True) + # Assume p will receive these signals and quit, which will + # then cause us to quit. + for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT): + signal.signal(sig, signal.SIG_IGN) + + filter_output(fix_stdout and p.stdout.fileno() or None, + fix_stderr and p.stderr.fileno() or None, + fix_stdout and sys.stdout.fileno() or None, + fix_stderr and sys.stderr.fileno() or None) + return p.wait() + except BaseException as ex: + add_ex_tb(ex) + try: + if p and p.poll() == None: + os.kill(p.pid, signal.SIGTERM) + p.wait() + except BaseException as kill_ex: + raise add_ex_ctx(add_ex_tb(kill_ex), ex) + raise ex + +wrap_main(lambda : run_subcmd(subcmd)) diff --git a/lib/bup/t/tclient.py b/lib/bup/t/tclient.py index f529c22..2ea4dca 100644 --- a/lib/bup/t/tclient.py +++ b/lib/bup/t/tclient.py @@ -17,7 +17,7 @@ def randbytes(sz): top_dir = os.path.realpath('../../..') -bup_exe = top_dir + '/bup' +bup_exe = top_dir + '/cmd/bup' s1 = randbytes(10000) s2 = randbytes(10000) diff --git a/lib/bup/t/tgit.py b/lib/bup/t/tgit.py index bcc58bd..2cf8230 100644 --- a/lib/bup/t/tgit.py +++ b/lib/bup/t/tgit.py @@ -12,7 +12,7 @@ from buptest import no_lingering_errors, test_tempdir top_dir = os.path.realpath('../../..') -bup_exe = top_dir + '/bup' +bup_exe = top_dir + '/cmd/bup' def exc(*cmd): diff --git a/main.py b/main.py deleted file mode 100755 index 05e62c4..0000000 --- a/main.py +++ /dev/null @@ -1,266 +0,0 @@ -#!/bin/sh -"""": # -*-python-*- # -*-python-*- -bup_python="$(dirname "$0")/cmd/bup-python" || exit $? -exec "$bup_python" "$0" ${1+"$@"} -""" -# end of bup preamble - -from __future__ import absolute_import, print_function -import errno, re, sys, os, subprocess, signal, getopt - -if sys.version_info[0] != 2 \ - and not os.environ.get('BUP_ALLOW_UNEXPECTED_PYTHON_VERSION') == 'true': - print('error: bup may crash with python versions other than 2, or eat your data', - file=sys.stderr) - sys.exit(2) - -from subprocess import PIPE -from sys import stderr, stdout -import select - -argv = sys.argv -exe = os.path.realpath(argv[0]) -exepath = os.path.split(exe)[0] or '.' -exeprefix = os.path.split(os.path.abspath(exepath))[0] - -# fix the PYTHONPATH to include our lib dir -if os.path.exists("%s/lib/bup/cmd/." % exeprefix): - # installed binary in /.../bin. - # eg. /usr/bin/bup means /usr/lib/bup/... is where our libraries are. - cmdpath = "%s/lib/bup/cmd" % exeprefix - libpath = "%s/lib/bup" % exeprefix - resourcepath = libpath -else: - # running from the src directory without being installed first - cmdpath = os.path.join(exepath, 'cmd') - libpath = os.path.join(exepath, 'lib') - resourcepath = libpath -sys.path[:0] = [libpath] -os.environ['PYTHONPATH'] = libpath + ':' + os.environ.get('PYTHONPATH', '') -os.environ['BUP_MAIN_EXE'] = os.path.abspath(exe) -os.environ['BUP_RESOURCE_PATH'] = resourcepath - - -from bup import compat, helpers -from bup.compat import add_ex_tb, add_ex_ctx, wrap_main -from bup.helpers import atoi, columnate, debug1, log, merge_dict, tty_width - - -def usage(msg=""): - log('Usage: bup [-?|--help] [-d BUP_DIR] [--debug] [--profile] ' - ' [options...]\n\n') - common = dict( - ftp = 'Browse backup sets using an ftp-like client', - fsck = 'Check backup sets for damage and add redundancy information', - fuse = 'Mount your backup sets as a filesystem', - help = 'Print detailed help for the given command', - index = 'Create or display the index of files to back up', - on = 'Backup a remote machine to the local one', - restore = 'Extract files from a backup set', - save = 'Save files into a backup set (note: run "bup index" first)', - tag = 'Tag commits for easier access', - web = 'Launch a web server to examine backup sets', - ) - - log('Common commands:\n') - for cmd,synopsis in sorted(common.items()): - log(' %-10s %s\n' % (cmd, synopsis)) - log('\n') - - log('Other available commands:\n') - cmds = [] - for c in sorted(os.listdir(cmdpath) + os.listdir(exepath)): - if c.startswith('bup-') and c.find('.') < 0: - cname = c[4:] - if cname not in common: - cmds.append(c[4:]) - log(columnate(cmds, ' ')) - log('\n') - - log("See 'bup help COMMAND' for more information on " + - "a specific command.\n") - if msg: - log("\n%s\n" % msg) - sys.exit(99) - - -if len(argv) < 2: - usage() - -# Handle global options. -try: - optspec = ['help', 'version', 'debug', 'profile', 'bup-dir='] - global_args, subcmd = getopt.getopt(argv[1:], '?VDd:', optspec) -except getopt.GetoptError as ex: - usage('error: %s' % ex.msg) - -help_requested = None -do_profile = False - -for opt in global_args: - if opt[0] in ['-?', '--help']: - help_requested = True - elif opt[0] in ['-V', '--version']: - subcmd = ['version'] - elif opt[0] in ['-D', '--debug']: - helpers.buglvl += 1 - os.environ['BUP_DEBUG'] = str(helpers.buglvl) - elif opt[0] in ['--profile']: - do_profile = True - elif opt[0] in ['-d', '--bup-dir']: - os.environ['BUP_DIR'] = opt[1] - else: - usage('error: unexpected option "%s"' % opt[0]) - -# Make BUP_DIR absolute, so we aren't affected by chdir (i.e. save -C, etc.). -if 'BUP_DIR' in os.environ: - os.environ['BUP_DIR'] = os.path.abspath(os.environ['BUP_DIR']) - -if len(subcmd) == 0: - if help_requested: - subcmd = ['help'] - else: - usage() - -if help_requested and subcmd[0] != 'help': - subcmd = ['help'] + subcmd - -if len(subcmd) > 1 and subcmd[1] == '--help' and subcmd[0] != 'help': - subcmd = ['help', subcmd[0]] + subcmd[2:] - -subcmd_name = subcmd[0] -if not subcmd_name: - usage() - -def subpath(s): - sp = os.path.join(exepath, 'bup-%s' % s) - if not os.path.exists(sp): - sp = os.path.join(cmdpath, 'bup-%s' % s) - return sp - -subcmd[0] = subpath(subcmd_name) -if not os.path.exists(subcmd[0]): - usage('error: unknown command "%s"' % subcmd_name) - -already_fixed = atoi(os.environ.get('BUP_FORCE_TTY')) -if subcmd_name in ['mux', 'ftp', 'help']: - already_fixed = True -fix_stdout = not already_fixed and os.isatty(1) -fix_stderr = not already_fixed and os.isatty(2) - -if fix_stdout or fix_stderr: - tty_env = merge_dict(os.environ, - {'BUP_FORCE_TTY': str((fix_stdout and 1 or 0) - + (fix_stderr and 2 or 0))}) -else: - tty_env = os.environ - - -sep_rx = re.compile(br'([\r\n])') - -def print_clean_line(dest, content, width, sep=None): - """Write some or all of content, followed by sep, to the dest fd after - padding the content with enough spaces to fill the current - terminal width or truncating it to the terminal width if sep is a - carriage return.""" - global sep_rx - assert sep in (b'\r', b'\n', None) - if not content: - if sep: - os.write(dest, sep) - return - for x in content: - assert not sep_rx.match(x) - content = b''.join(content) - if sep == b'\r' and len(content) > width: - content = content[width:] - os.write(dest, content) - if len(content) < width: - os.write(dest, b' ' * (width - len(content))) - if sep: - os.write(dest, sep) - -def filter_output(src_out, src_err, dest_out, dest_err): - """Transfer data from src_out to dest_out and src_err to dest_err via - print_clean_line until src_out and src_err close.""" - global sep_rx - assert not isinstance(src_out, bool) - assert not isinstance(src_err, bool) - assert not isinstance(dest_out, bool) - assert not isinstance(dest_err, bool) - assert src_out is not None or src_err is not None - assert (src_out is None) == (dest_out is None) - assert (src_err is None) == (dest_err is None) - pending = {} - pending_ex = None - try: - fds = tuple([x for x in (src_out, src_err) if x is not None]) - while fds: - ready_fds, _, _ = select.select(fds, [], []) - width = tty_width() - for fd in ready_fds: - buf = os.read(fd, 4096) - dest = dest_out if fd == src_out else dest_err - if not buf: - fds = tuple([x for x in fds if x is not fd]) - print_clean_line(dest, pending.pop(fd, []), width) - else: - split = sep_rx.split(buf) - while len(split) > 1: - content, sep = split[:2] - split = split[2:] - print_clean_line(dest, - pending.pop(fd, []) + [content], - width, - sep) - assert(len(split) == 1) - if split[0]: - pending.setdefault(fd, []).extend(split) - except BaseException as ex: - pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex) - try: - # Try to finish each of the streams - for fd, pending_items in compat.items(pending): - dest = dest_out if fd == src_out else dest_err - try: - print_clean_line(dest, pending_items, width) - except (EnvironmentError, EOFError) as ex: - pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex) - except BaseException as ex: - pending_ex = add_ex_ctx(add_ex_tb(ex), pending_ex) - if pending_ex: - raise pending_ex - -def run_subcmd(subcmd): - - c = (do_profile and [sys.executable, '-m', 'cProfile'] or []) + subcmd - if not (fix_stdout or fix_stderr): - os.execvp(c[0], c) - - p = None - try: - p = subprocess.Popen(c, - stdout=PIPE if fix_stdout else sys.stdout, - stderr=PIPE if fix_stderr else sys.stderr, - env=tty_env, bufsize=4096, close_fds=True) - # Assume p will receive these signals and quit, which will - # then cause us to quit. - for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT): - signal.signal(sig, signal.SIG_IGN) - - filter_output(fix_stdout and p.stdout.fileno() or None, - fix_stderr and p.stderr.fileno() or None, - fix_stdout and sys.stdout.fileno() or None, - fix_stderr and sys.stderr.fileno() or None) - return p.wait() - except BaseException as ex: - add_ex_tb(ex) - try: - if p and p.poll() == None: - os.kill(p.pid, signal.SIGTERM) - p.wait() - except BaseException as kill_ex: - raise add_ex_ctx(add_ex_tb(kill_ex), ex) - raise ex - -wrap_main(lambda : run_subcmd(subcmd))