├── .gitignore
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── sequestrum
├── __init__.py
├── arguments.py
├── directories.py
├── logging.py
├── options.py
├── sequestrum.py
└── symlink.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg[s]
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Environments
51 | .env
52 | .venv
53 | env/
54 | venv/
55 | ENV/
56 | env.bak/
57 | venv.bak/
58 |
59 | # IDEs
60 | .vscode/
61 |
62 | # mypy
63 | .mypy_cache/
64 | .dmypy.json
65 | dmypy.json
66 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contribution Guidelines
2 |
3 | **Note**: contributing implies that your contributions get licensed under the terms of [LICENSE.md](LICENSE.md) (MIT).
4 |
5 | ## Opening issues
6 |
7 | 1. Make sure you have a GitHub account.
8 | 2. Include steps to reproduce, if it is a bug.
9 | 3. Include information on what version you are using.
10 |
11 | ## Submit changes
12 |
13 | Commit messages should be both _clear_ and _descriptive_. If possible they should start with the _verb_ that describes the change.
14 | Don't be shy to include additional information, like motivation, for that change below the initial line.
15 |
16 | Other developers should be able to understand _why_ a change occurred, when looking at it at a later point in time.
17 |
18 | ### Coding Style
19 |
20 | This project follows the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide.
21 | Make sure you adhere to the coding style, or your pull request might not get merged.
22 |
23 | ---
24 |
25 | That said, if anything is unclear or I missed something feel free to open an issue.
26 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Ivy Zhang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Sequestrum
3 |
4 |
5 | ## Important
6 | No longer being maintained, we'll see if I start work on this again.
7 |
8 | ## Description
9 | A modern, lightweight dotfile manager for the masses. This README.md may look daunting, but trust me on this, it's as easy as pie.
10 | Promise. Now you may be wondering, why use this over Stow or Dotbot. Simple.Well for starters, the name is better. Do you need any
11 | other reasons? Fine, fine.. some real reasons.This is specifically made for dotfile management and provides features like modularity,
12 | dotfile repository setup, and more!
13 |
14 | ## Install Guide
15 | - Arch: `yay -S sequestrum-git`
16 | - Pip: `pip install --user sequestrum`
17 |
18 | ## Wiki
19 | For help on writing a config and setting up Sequestrum, visit the [Wiki](https://github.com/iiPlasma/sequestrum/wiki)
20 |
21 | ## Contributing
22 | If you'd like to contribute, whether that'd be improving the comments or README (which could always use work), or adding functionality,
23 | just fork this, add to it, then send a merge request. I'll try and get around to looking at it as soon as possible.
24 | Be sure to read [CONTRIBUTING.md](CONTRIBUTING.md) first though.
25 |
26 | ## License
27 | Copyright (c) 2018-2018 Ivy Zhang. Released under the MIT License.
28 |
--------------------------------------------------------------------------------
/sequestrum/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iyzg/sequestrum/fdcdc244603a00aeb5fa8537c4edc5ed05be329c/sequestrum/__init__.py
--------------------------------------------------------------------------------
/sequestrum/arguments.py:
--------------------------------------------------------------------------------
1 | # Arguments Modules
2 |
3 | # Imports
4 | import argparse
5 |
6 |
7 | def get_arguments():
8 | """
9 | Return the arguments in the form of a tuple
10 | """
11 |
12 | parser = argparse.ArgumentParser()
13 | group = parser.add_mutually_exclusive_group()
14 |
15 | group.add_argument("-i", "--install", help="Install packages onto local system. Use all to install all packages.")
16 | group.add_argument("-r", "--refresh", help="Refresh your dotfiles based on your config.", action="store_true")
17 | group.add_argument("-u", "--unlink", help="Unlink packages from local system. Use all to unlink all packages.")
18 |
19 | args = parser.parse_args()
20 |
21 | if args.install is not None:
22 | return ("Install", args.install)
23 | elif args.refresh:
24 | return ("Refresh", "all")
25 | elif args.unlink is not None:
26 | return ("Unlink", args.unlink)
27 |
--------------------------------------------------------------------------------
/sequestrum/directories.py:
--------------------------------------------------------------------------------
1 | # Directory Module
2 |
3 | # Libraries
4 | import os
5 | import shutil
6 | import pathlib
7 | import sequestrum.logging as logging
8 |
9 |
10 | def current_path():
11 | """
12 | Returns the current path
13 | """
14 |
15 | return os.getcwd()
16 |
17 |
18 | def create_folder(path):
19 | """
20 | Creates a folder
21 | """
22 |
23 | try:
24 | os.makedirs(path)
25 | except OSError as error:
26 | logging.print_error("Could not create folder \"{}\" due to following error: {}".format(path, error))
27 | else:
28 | logging.print_verbose("Folder doesn't exist and was created: {}".format(path))
29 |
30 |
31 | def create_base_folder(path):
32 | """
33 | Create base directory if needed
34 | """
35 |
36 | basePath = pathlib.Path(path).parent
37 |
38 | # Check if the base folder is a file
39 | if basePath.exists():
40 | # Check if the parent is a file or if its a symlink
41 | if basePath.is_file() or basePath.is_symlink():
42 | logging.print_error("Base directory is a file or link: {}".format(basePath))
43 | return False
44 | # If not, it must be a directory, so we are ok
45 | else:
46 | return True
47 |
48 | # Create path and parents (or ignore if folder already exists)
49 | try:
50 | basePath.mkdir(parents=True, exist_ok=True)
51 | except Exception as error:
52 | logging.print_error("Could not create parent folder \"{}\" due to following error: {}".format(basePath, error))
53 | return False
54 | else:
55 | logging.print_verbose("Parent folder dosent exist and was created: {}".format(basePath))
56 |
57 | return True
58 |
59 |
60 | def delete_folder(path):
61 | """
62 | Deletes a folder
63 | """
64 |
65 | basePath = pathlib.Path(path)
66 |
67 | if not basePath.exists():
68 | logging.print_error("Folder doesn't exist: {}".format(path))
69 | return
70 |
71 | try:
72 | if basePath.is_symlink():
73 | basePath.unlink()
74 | else:
75 | shutil.rmtree(basePath)
76 | except OSError as error:
77 | logging.print_error("Folder Deletion/Unsymlink Failed: {}".format(error))
78 | else:
79 | logging.print_verbose("Folder Deleted Successfully: {}".format(path))
80 |
81 |
82 | def delete_file(path):
83 | """
84 | Deletes file
85 | """
86 |
87 | try:
88 | os.remove(path)
89 | except OSError as error:
90 | logging.print_error("File Deletion Failed: {}".format(path))
91 | else:
92 | logging.print_verbose("File Successfully Deleted: {}".format(path))
93 |
94 |
95 | def is_folder(path):
96 | """
97 | Checks to see if path is a folder
98 | """
99 |
100 | try:
101 | if os.path.isdir(path):
102 | return True
103 | else:
104 | return False
105 | except OSError as error:
106 | logging.print_error("Path doesn't exist: {}".format(path))
107 |
108 |
109 | def is_file(path):
110 | """
111 | Checks to see if path is a file
112 | """
113 |
114 | try:
115 | if os.path.isfile(path):
116 | return True
117 | else:
118 | return False
119 | except OSError as error:
120 | logging.print_error("Path doesn't exist: {}".format(path))
121 |
122 |
123 | def grab_package_names(path):
124 | """
125 | Grabs package names from config
126 | """
127 |
128 | package_list = []
129 | for name in os.listdir(path):
130 | if os.path.isdir(path):
131 | package_list.append(name)
132 |
133 | return package_list
134 |
--------------------------------------------------------------------------------
/sequestrum/logging.py:
--------------------------------------------------------------------------------
1 | # Logging module
2 |
3 | from sys import exit
4 |
5 |
6 | def format_output(error_type, error_message):
7 | """
8 | Formats the errors to look standard
9 | """
10 | return "[{}] {}".format(error_type, error_message)
11 |
12 |
13 | def print_fatal(error_message):
14 | """
15 | Prints message with FATAL
16 | """
17 | print("\033[1;31mFATAL\033[0m {}".format(error_message))
18 | exit()
19 |
20 |
21 | def print_error(error_message):
22 | """
23 | Prints message with ERROR
24 | """
25 | print("\033[1;31mERROR\033[0m {}".format(error_message))
26 |
27 |
28 | def print_warn(error_message):
29 | """
30 | Prints message with WARN
31 | """
32 | print("\033[1;33mWARN\033[0m {}".format(error_message))
33 |
34 |
35 | def print_info(error_message):
36 | """
37 | Prints message with INFO
38 | """
39 | print("\033[1;32mINFO\033[0m {}".format(error_message))
40 |
41 |
42 | def print_verbose(error_message):
43 | """
44 | Prints message with green color to show success
45 | """
46 | print("\033[1;32mVERBOSE\033[0m {}".format(error_message))
47 |
--------------------------------------------------------------------------------
/sequestrum/options.py:
--------------------------------------------------------------------------------
1 | # Options Module
2 |
3 | # Imports
4 | from subprocess import run
5 | import sequestrum.logging as logging
6 |
7 |
8 | def run_commands(unparsed_command_list, package_name=None):
9 | """
10 | Runs commands passed in
11 | """
12 |
13 | for command in unparsed_command_list:
14 | parsed_command = command.split()
15 |
16 | try:
17 | runner = run(parsed_command)
18 | except Exception as error:
19 | logging.print_fatal("Error occured during command \"{}\": {}".format(command, error), package_name)
20 | else:
21 | logging.print_verbose("Command \"{}\" finished with exit code: {}".format(command, runner.returncode), package_name)
22 |
--------------------------------------------------------------------------------
/sequestrum/sequestrum.py:
--------------------------------------------------------------------------------
1 | # Libraries
2 | from pathlib import Path
3 | import sys
4 | import yaml
5 |
6 | # Modules
7 | import sequestrum.directories as directories
8 | import sequestrum.symlink as symlink
9 | import sequestrum.arguments as arguments
10 | import sequestrum.options as options
11 | import sequestrum.logging as logging
12 |
13 | # For Later
14 | packages_to_unlink = []
15 |
16 | # Global constants
17 | HOME_PATH = str(Path.home()) + "/"
18 |
19 |
20 | # Creates a new directory. It creates a new folder path using the config
21 | # then creates a new folder using that path. It then loops through each
22 | # link in the links list and **copies** (not symlinking) the original file
23 | # on the source system over to the dotfiles.
24 |
25 |
26 | def setup_package(package_key, config_dict, dotfile_path):
27 | """
28 | Setup package directory on dotfile
29 | """
30 |
31 | # Make a path for the new directory path using the name specified in the
32 | # config then make the folder using the path.
33 | package_config = config_dict['options'][package_key]
34 | new_package_path = dotfile_path + package_config['directoryName'] + "/"
35 | if directories.is_folder(new_package_path) is False:
36 | directories.create_folder(new_package_path)
37 |
38 | for link in package_config['links']:
39 | for key, value in link.items():
40 | source_file = HOME_PATH + value
41 | dest_file = new_package_path + key
42 |
43 | # Checks
44 | if directories.is_folder(dest_file):
45 | logging.print_warn("Folder exists, skipping: {}".format(dest_file))
46 | continue
47 | elif directories.is_file(dest_file):
48 | logging.print_warn("File exists, skipping: {}".format(dest_file))
49 | continue
50 |
51 | # Setup
52 | if directories.is_folder(source_file):
53 | symlink.copy_folder(source_file, dest_file)
54 | directories.delete_folder(source_file)
55 | elif directories.is_file(source_file):
56 | symlink.copy_file(source_file, dest_file)
57 | directories.delete_file(source_file)
58 | else:
59 | return False
60 |
61 | return True
62 |
63 |
64 | # Grabs the directory of the key. For each item in the dotfile directory,
65 | # properly symlink the file to the right place. If the file on the local
66 | # system already exists. Delete the existing file before symlinking to
67 | # prevent issues.
68 |
69 |
70 | def install_package(package_key, config_dict, dotfile_path):
71 | """
72 | Install package to local system
73 | """
74 |
75 | # Grab dotfile package directory
76 | package_config = config_dict['options'][package_key]
77 | directory_path = dotfile_path + package_config['directoryName'] + "/"
78 |
79 | # Loop through files to link
80 | for link in package_config['links']:
81 | # Symlink files to local files
82 | for key, value in link.items():
83 | source_file = directory_path + key
84 | dest_file = HOME_PATH + value
85 |
86 | if directories.is_folder(dest_file):
87 | logging.print_warn("Folder exists, skipping: {}".format(dest_file))
88 | continue
89 | elif directories.is_file(dest_file):
90 | logging.print_warn("File exists, skipping: {}".format(dest_file))
91 | continue
92 |
93 | if directories.create_base_folder(dest_file):
94 | symlink.create_symlink(source_file, dest_file)
95 | else:
96 | return False
97 |
98 | return True
99 |
100 |
101 | def get_packages_to_unlink(package_key, config_dict, dotfile_path):
102 | """
103 | Grab packages and put them into a list ( NO DUPES )
104 | """
105 |
106 | package_config = config_dict['options'][package_key]
107 |
108 | for link in package_config['links']:
109 | for _, value in link.items():
110 | fileToGrab = HOME_PATH + value
111 |
112 | if fileToGrab not in packages_to_unlink:
113 | packages_to_unlink.append(fileToGrab)
114 |
115 |
116 | def unlink_packages():
117 | """
118 | Unlink all files in packages_to_unlink
119 | """
120 | for path in packages_to_unlink:
121 | if directories.is_folder(path):
122 | directories.delete_folder(path)
123 | elif directories.is_file(path):
124 | directories.delete_file(path)
125 | else:
126 | logging.print_error("Nothing to unlink")
127 |
128 | # Goes through all the file locations that need to be empty for the
129 | # symlinking to work and checks to see if they're empty. If they're not,
130 | # it will return false. If it is clean, it'll return true.
131 |
132 |
133 | def check_localfile_locations(package_key, config_dict, mode=None):
134 | """
135 | Checks local file locations
136 | """
137 |
138 | if mode is None:
139 | logging.print_fatal("No mode provided to check")
140 |
141 | for link in config_dict['options'][package_key]['links']:
142 | for key, value in link.items():
143 | destPath = HOME_PATH + value
144 |
145 | if mode == "Clean":
146 | if symlink.symlink_source_exists(destPath):
147 | logging.print_fatal("Local file location occupied: {}".format(destPath))
148 | return False
149 | elif mode == "Dirty":
150 | if not symlink.symlink_source_exists(destPath):
151 | logging.print_fatal("Local file location empty: {}".format(destPath))
152 | return False
153 |
154 | return True
155 |
156 | # Checks to see if the file locations in the dotfile repository exist. If
157 | # they do, return false. If they don't, return true. This is to prevent
158 | # overwriting of file that may or may not be important to the user.
159 |
160 |
161 | def check_dotfile_locations(package_key, config_dict, dotfile_path, mode=None):
162 | """
163 | Checks dotfile locations
164 | """
165 |
166 | if mode is None:
167 | logging.print_fatal("No mode provided to check")
168 |
169 | directory_path = dotfile_path + \
170 | config_dict['options'][package_key]['directoryName'] + "/"
171 |
172 | for link in config_dict['options'][package_key]['links']:
173 | for key, value in link.items():
174 | sourcePath = directory_path + key
175 |
176 | if mode == "Clean":
177 | if symlink.symlink_source_exists(sourcePath):
178 | logging.print_fatal("Dotfile location occupied: {}".format(sourcePath))
179 | return False
180 | if mode == "Dirty":
181 | if not symlink.symlink_source_exists(sourcePath):
182 | logging.print_fatal("Dotfile location empty: {}".format(sourcePath))
183 | return False
184 |
185 | return True
186 |
187 |
188 | def main():
189 | # Grab user inputted args from the module and make sure they entered some.
190 | args = arguments.get_arguments()
191 |
192 | if args is None:
193 | logging.print_error("Must pass arguments")
194 | sys.exit()
195 |
196 | config_file = None
197 | config_dict = {}
198 | package_list = []
199 |
200 | try:
201 | config_file = open("config.yaml", "r")
202 | except IOError:
203 | logging.print_error("No configuration found")
204 | sys.exit()
205 |
206 | config_dict = yaml.load(config_file)
207 |
208 | # Grab list of directories from the config.
209 | for key, value in config_dict['options'].items():
210 | if key.endswith("Package"):
211 | friendly_name = key[:-7]
212 | config_dict['options'][key]['package_name'] = friendly_name
213 | package_list.append(friendly_name)
214 |
215 | # Error checking for proper config
216 | if "base" not in config_dict['options']:
217 | logging.print_fatal(
218 | "Invalid config file, a base package needs to be defined")
219 | elif "dotfileDirectory" not in config_dict['options']['base']:
220 | logging.print_fatal("Missing dotfileDirectory in base package")
221 |
222 | # Grab the path of the dotfile directory
223 | dotfile_path = HOME_PATH + \
224 | config_dict['options']['base']['dotfileDirectory'] + "/"
225 |
226 | # Install the files from the dotfiles. Symlinks the files from the
227 | # specified packages to the local system files. If the file or folder
228 | # already exists on the local system, delete it then symlink properly to
229 | # avoid errors.
230 | if args[0] == "Install":
231 | # Install all packages
232 | if args[1] == "all":
233 | for key, value in config_dict['options'].items():
234 | if key.endswith("Package"):
235 | check_dotfile_locations(key, config_dict, dotfile_path, "Dirty")
236 |
237 | for key, value in config_dict['options'].items():
238 | if key.endswith("Package"):
239 | if "commandsBefore" in value:
240 | options.run_commands(
241 | config_dict['options'][key]['commandsBefore'], config_dict['options'][key]['package_name'])
242 | install_package(key, config_dict, dotfile_path)
243 | if "commandsAfter" in value:
244 | options.run_commands(
245 | config_dict['options'][key]['commandsAfter'], config_dict['options'][key]['package_name'])
246 |
247 | logging.print_info("Complete installation complete")
248 |
249 | # The option to only install one package instead of all your dotfiles.
250 | elif args[1] in package_list:
251 | for key, value in config_dict['options'].items():
252 | if key == args[1] + "Package":
253 | check_dotfile_locations(key, config_dict, dotfile_path, "Dirty")
254 |
255 | for key, value in config_dict['options'].items():
256 | if key == args[1] + "Package":
257 | if "commandsBefore" in value:
258 | options.run_commands(
259 | config_dict['options'][key]["commandsBefore"])
260 | install_package(key, config_dict, dotfile_path)
261 | if "commandsAfter" in value:
262 | options.run_commands(
263 | config_dict['options'][key]["commandsAfter"])
264 |
265 | logging.print_info("{} installation complete".format(args[1]))
266 | else:
267 | logging.print_error("Invalid Package")
268 |
269 | # Refresh
270 | # -------
271 | # Refreshs the symlinks with the current file. This is so users don't have to manually
272 | # move files around and can instead manage their dotfiles in one file.
273 | # TODO: Remove files with warning if they are gone from the config
274 | elif args[0] == "Refresh":
275 | if args[1] == "all":
276 | for key, value in config_dict['options'].items():
277 | if key.endswith("Package"):
278 | if "commandsBefore" in value:
279 | options.run_commands(
280 | config_dict['options'][key]["commandsBefore"])
281 | setup_package(key, config_dict, dotfile_path)
282 | if "commandsAfter" in value:
283 | options.run_commands(
284 | config_dict['options'][key]["commandsAfter"])
285 |
286 | logging.print_info("Dotfile refresh complete")
287 | else:
288 | logging.print_error("Error 102 Please report to GH")
289 |
290 | # Unlink the source files. This doesn't really "unlink", instead it actually just
291 | # deletes the files. It collects a list of files to unlink then it goes through and
292 | # unlinks them all.
293 | # TODO: Add safety to make sure both the files exist
294 | elif args[0] == "Unlink":
295 | if args[1] == "all":
296 | for key, value in config_dict['options'].items():
297 | if key.endswith("Package"):
298 | get_packages_to_unlink(key, config_dict, dotfile_path)
299 | unlink_packages()
300 | logging.print_info("Completele unlink complete")
301 | elif args[1] in package_list:
302 | for key, value in config_dict['options'].items():
303 | if key == args[1] + "Package":
304 | get_packages_to_unlink(key, config_dict, dotfile_path)
305 | unlink_packages()
306 | logging.print_info("{} unlink complete".format(args[1]))
307 | else:
308 | logging.print_error("Invalid Package")
309 |
310 | else:
311 | logging.print_error("Invalid Command")
312 |
313 |
314 | if __name__ == '__main__':
315 | main()
316 |
--------------------------------------------------------------------------------
/sequestrum/symlink.py:
--------------------------------------------------------------------------------
1 | # Symlink Module
2 |
3 | # Libraries
4 | import os
5 | from shutil import copytree, copyfile
6 | import sequestrum.logging as logging
7 |
8 |
9 | def create_symlink(source, destination):
10 | """
11 | Creates symlink from source to destination
12 | """
13 |
14 | try:
15 | os.symlink(source, destination)
16 | except OSError as error:
17 | logging.print_error("Unable to create symlink: {}".format(error))
18 | else:
19 | logging.print_verbose("Linking {} <-> {}".format(source, destination))
20 |
21 |
22 | def copy_file(source, destination):
23 | """
24 | Copys file from source to destination
25 | """
26 |
27 | try:
28 | copyfile(source, destination)
29 | except IOError as error:
30 | logging.print_error("Unable to copy file: {}".format(error))
31 | else:
32 | logging.print_verbose("Copied {} --> {}".format(source, destination))
33 |
34 |
35 | def copy_folder(source, destination):
36 | """
37 | Copies frolder from source to destination
38 | """
39 |
40 | try:
41 | copytree(source, destination)
42 | except IOError as error:
43 | logging.print_error("Unable to copy folder: {}".format(error))
44 | else:
45 | logging.print_verbose("Copied {} --> {}".format(source, destination))
46 |
47 |
48 | def symlink_source_exists(sourcePath):
49 | """
50 | Checks to see if symlink source exists
51 | """
52 |
53 | # Check if file exists
54 | if not os.path.exists(sourcePath):
55 | return False
56 |
57 | # We cannot link symlinks
58 | if os.path.islink(sourcePath):
59 | return False
60 |
61 | return os.path.exists(sourcePath)
62 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='sequestrum',
5 | description='Modern Dotfile Manager',
6 | long_description=open('README.md').read(),
7 | long_description_content_type='text/markdown',
8 | use_scm_version=True,
9 | setup_requires=['setuptools_scm'],
10 | url='http://github.com/iiPlasma/sequestrum',
11 | author='Ivy Zhang',
12 | author_email="ZIvy042003@gmail.com",
13 | license='MIT',
14 | packages=['sequestrum'],
15 | install_requires=[
16 | 'pyyaml == 5.1',
17 | ],
18 | entry_points={
19 | 'console_scripts':
20 | ['sequestrum = sequestrum.sequestrum:main']
21 | }
22 | )
23 |
--------------------------------------------------------------------------------