| 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 = """ |
| Tom Anderson | 1c9a30e | 2024-06-06 00:55:30 | [diff] [blame] | 7 | Make a symlink. |
| Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 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 |
| Tom Anderson | 0a58820 | 2024-06-03 23:46:20 | [diff] [blame] | 22 | import os |
| 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') |
| [email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 32 | |
| [email protected] | 4afc8d67 | 2013-05-28 21:49:11 | [diff] [blame] | 33 | options, args = parser.parse_args(argv[1:]) |
| [email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 34 | if len(args) < 2: |
| 35 | parser.error('at least two arguments required.') |
| 36 | |
| 37 | target = args[-1] |
| 38 | sources = args[:-1] |
| 39 | for s in sources: |
| 40 | t = os.path.join(target, os.path.basename(s)) |
| agrieve | 6cc97ff4 | 2015-07-15 20:13:15 | [diff] [blame] | 41 | if len(sources) == 1 and not os.path.isdir(target): |
| 42 | t = target |
| bpastene | a516b28 | 2016-03-21 17:52:28 | [diff] [blame] | 43 | t = os.path.expanduser(t) |
| Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 44 | if os.path.realpath(t) == os.path.realpath(s): |
| bpastene | e1758d3 | 2016-01-26 00:31:50 | [diff] [blame] | 45 | continue |
| [email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 46 | try: |
| Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 47 | # N.B. Python 2.x does not have os.symlink for Windows. |
| 48 | # Python 3 has os.symlink for Windows, but requires either the admin- |
| 49 | # granted privilege SeCreateSymbolicLinkPrivilege or, as of Windows 10 |
| 50 | # 1703, that Developer Mode be enabled. Hard links and junctions do not |
| 51 | # require any extra privileges to create. |
| 52 | if os.name == 'nt': |
| 53 | # mklink does not tolerate /-delimited path names. |
| 54 | t = t.replace('/', '\\') |
| 55 | s = s.replace('/', '\\') |
| 56 | # N.B. This tool only handles file hardlinks, not directory junctions. |
| 57 | subprocess.check_output(['cmd.exe', '/c', 'mklink', '/H', t, s], |
| 58 | stderr=subprocess.STDOUT) |
| 59 | else: |
| 60 | os.symlink(s, t) |
| Raul Tambre | f7d4453b | 2019-09-26 19:20:53 | [diff] [blame] | 61 | except OSError as e: |
| [email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 62 | if e.errno == errno.EEXIST and options.force: |
| eseidel | add100b | 2015-07-01 19:09:40 | [diff] [blame] | 63 | if os.path.isdir(t): |
| 64 | shutil.rmtree(t, ignore_errors=True) |
| 65 | else: |
| 66 | os.remove(t) |
| [email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 67 | os.symlink(s, t) |
| 68 | else: |
| 69 | raise |
| Raul Tambre | f7d4453b | 2019-09-26 19:20:53 | [diff] [blame] | 70 | except subprocess.CalledProcessError as e: |
| Jon Kunkee | c89a09e | 2019-02-06 22:57:24 | [diff] [blame] | 71 | # Since subprocess.check_output does not return an easily checked error |
| 72 | # number, in the 'force' case always assume it is 'file already exists' |
| 73 | # and retry. |
| 74 | if options.force: |
| 75 | if os.path.isdir(t): |
| 76 | shutil.rmtree(t, ignore_errors=True) |
| 77 | else: |
| 78 | os.remove(t) |
| 79 | subprocess.check_output(e.cmd, stderr=subprocess.STDOUT) |
| 80 | else: |
| 81 | raise |
| [email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 82 | |
| 83 | |
| [email protected] | a2e338ed | 2013-01-22 17:57:14 | [diff] [blame] | 84 | if __name__ == '__main__': |
| 85 | sys.exit(Main(sys.argv)) |