Takuto Ikuta | 3dab32e0 | 2023-01-12 18:52:00 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Avi Drissman | 73a09d1 | 2022-09-08 20:33:38 | [diff] [blame] | 2 | # Copyright 2013 The Chromium Authors |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 6 | description = """ |
| 7 | Make a symlink and optionally touch a file (to handle dependencies). |
| 8 | """ |
| 9 | usage = "%prog [options] source[ source ...] linkname" |
Nico Weber | 4054b0c | 2021-03-28 18:23:22 | [diff] [blame] | 10 | epilog = """\ |
| 11 | A symlink to source is created at linkname. If multiple sources are specified, |
Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 12 | then linkname is assumed to be a directory, and will contain all the links to |
bpastene | a516b28 | 2016-03-21 17:52:28 | [diff] [blame] | 13 | the sources (basenames identical to their source). |
Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 14 | |
| 15 | On Windows, this will use hard links (mklink /H) to avoid requiring elevation. |
| 16 | This means that if the original is deleted and replaced, the link will still |
Nico Weber | 4054b0c | 2021-03-28 18:23:22 | [diff] [blame] | 17 | have the old contents. |
bpastene | a516b28 | 2016-03-21 17:52:28 | [diff] [blame] | 18 | """ |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 19 | |
| 20 | import errno |
| 21 | import optparse |
| 22 | import os.path |
eseidel | add100b | 2015-07-01 19:09:40 | [diff] [blame] | 23 | import shutil |
Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 24 | import subprocess |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 25 | import sys |
| 26 | |
| 27 | |
| 28 | def Main(argv): |
Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 29 | parser = optparse.OptionParser(usage=usage, description=description, |
| 30 | epilog=epilog) |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 31 | parser.add_option('-f', '--force', action='store_true') |
| 32 | parser.add_option('--touch') |
| 33 | |
[email protected] | 4afc8d67 | 2013-05-28 21:49:11 | [diff] [blame] | 34 | options, args = parser.parse_args(argv[1:]) |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 35 | if len(args) < 2: |
| 36 | parser.error('at least two arguments required.') |
| 37 | |
| 38 | target = args[-1] |
| 39 | sources = args[:-1] |
| 40 | for s in sources: |
| 41 | t = os.path.join(target, os.path.basename(s)) |
agrieve | 6cc97ff4 | 2015-07-15 20:13:15 | [diff] [blame] | 42 | if len(sources) == 1 and not os.path.isdir(target): |
| 43 | t = target |
bpastene | a516b28 | 2016-03-21 17:52:28 | [diff] [blame] | 44 | t = os.path.expanduser(t) |
Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 45 | if os.path.realpath(t) == os.path.realpath(s): |
bpastene | e1758d3 | 2016-01-26 00:31:50 | [diff] [blame] | 46 | continue |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 47 | try: |
Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 48 | # N.B. Python 2.x does not have os.symlink for Windows. |
| 49 | # Python 3 has os.symlink for Windows, but requires either the admin- |
| 50 | # granted privilege SeCreateSymbolicLinkPrivilege or, as of Windows 10 |
| 51 | # 1703, that Developer Mode be enabled. Hard links and junctions do not |
| 52 | # require any extra privileges to create. |
| 53 | if os.name == 'nt': |
| 54 | # mklink does not tolerate /-delimited path names. |
| 55 | t = t.replace('/', '\\') |
| 56 | s = s.replace('/', '\\') |
| 57 | # N.B. This tool only handles file hardlinks, not directory junctions. |
| 58 | subprocess.check_output(['cmd.exe', '/c', 'mklink', '/H', t, s], |
| 59 | stderr=subprocess.STDOUT) |
| 60 | else: |
| 61 | os.symlink(s, t) |
Raul Tambre | f7d4453b | 2019-09-26 19:20:53 | [diff] [blame] | 62 | except OSError as e: |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 63 | if e.errno == errno.EEXIST and options.force: |
eseidel | add100b | 2015-07-01 19:09:40 | [diff] [blame] | 64 | if os.path.isdir(t): |
| 65 | shutil.rmtree(t, ignore_errors=True) |
| 66 | else: |
| 67 | os.remove(t) |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 68 | os.symlink(s, t) |
| 69 | else: |
| 70 | raise |
Raul Tambre | f7d4453b | 2019-09-26 19:20:53 | [diff] [blame] | 71 | except subprocess.CalledProcessError as e: |
Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 72 | # Since subprocess.check_output does not return an easily checked error |
| 73 | # number, in the 'force' case always assume it is 'file already exists' |
| 74 | # and retry. |
| 75 | if options.force: |
| 76 | if os.path.isdir(t): |
| 77 | shutil.rmtree(t, ignore_errors=True) |
| 78 | else: |
| 79 | os.remove(t) |
| 80 | subprocess.check_output(e.cmd, stderr=subprocess.STDOUT) |
| 81 | else: |
| 82 | raise |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 83 | |
| 84 | |
| 85 | if options.touch: |
Joshua Pawlicki | 45c33fb7 | 2023-02-13 19:52:43 | [diff] [blame] | 86 | os.makedirs(os.path.dirname(options.touch), exist_ok=True) |
Nico Weber | 4054b0c | 2021-03-28 18:23:22 | [diff] [blame] | 87 | with open(options.touch, 'w'): |
[email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 88 | pass |
| 89 | |
| 90 | |
| 91 | if __name__ == '__main__': |
| 92 | sys.exit(Main(sys.argv)) |