fine we'll just copy files on Windows/WSL

master
Jordan Orelli 2 years ago
parent 3c45a31b70
commit b29d26f7f8

@ -1,9 +1,12 @@
# items in the [home] section are copied into the home directory with the same
# relative location as they appear in the dotfiles repo
[home]
dirs=
files=
# sync vim plugin directories but not all of .vim
.vim/ftplugin
.vim/pack
files=
# individual files to sync
.bash_profile
.bashrc
.screenrc
@ -11,3 +14,13 @@ files=
.vimrc
.config/htop/htoprc
.config/lazydocker/config.yml
# items in map.posix define a place where config files will be placed on posix
# systems relative to the user's home directory
[map.posix]
nvim/init.vim=.config/nvim/init.vim
# items in the map.windows section defines a place where config files will be
# placed on windows, relative to the user's home directory
[map.windows]
nvim/init.vim=AppData/Local/nvim/init.vim

@ -2,5 +2,6 @@
"""
installs preferences
"""
from installer import install
install()
from installer import Installer
installer = Installer()
installer.run()

@ -14,13 +14,7 @@ from functools import cached_property
from installer import host
from installer.options import Options
from installer import log
def install():
"""
runs our install process based on cli arguments
"""
installer = Installer()
installer.run()
from installer import targets
class Installer:
"""
@ -28,86 +22,75 @@ class Installer:
"""
def __init__(self):
self.options = Options.from_cli_args()
self.config_path = "include.json"
def run(self):
"""
runs the install process
"""
if home_files := self.config.get('home_files'):
for name in home_files:
self.install_home_file(name)
if host.is_windows and not host.is_admin:
print("You are not admin: admin is required on Windows")
os.exit(1)
print("linking in home files")
home = self.options.config['home']
home_files = filter(None, home['files'].splitlines())
for fname in home_files:
print(f"\n{fname}")
path = pathlib.Path(fname)
self.map_file(path, path)
if host.is_linux:
self.map_section('map.posix')
self.map_section('map.windows')
def install_home_file(self, name):
if host.is_windows:
self.map_section('map.windows')
@cached_property
def targets(self):
"""
installs a given file
defines all of the places where preferences files will be installed
"""
log.debug("install: %s", name)
if host.is_linux:
return [targets.Linux(), targets.WSLHost()]
if host.is_windows:
return [targets.Windows()]
return []
def map_file(self, source_path, target_path):
if not source_path.is_absolute():
source_path = host.dotfiles_root / source_path
source_path = self.prefs_dir / name
log.debug(" source: %s", source_path)
print(f"source path: {source_path}")
print(f"source drive: {source_path.drive}")
if not source_path.exists():
log.warning("home file source path %s does not exist", source_path)
print('skip: no such file')
return
if not source_path.is_file():
log.warning("home file source path %s is not a file", source_path)
return
for target in self.targets:
target.map_file(source_path, target_path)
if host.is_linux:
target_path = pathlib.Path.home() / name
log.debug(" target: %s", target_path)
if target_path.exists():
log.debug(" target path exists, will remove")
target_path.unlink()
log.debug(" link: %s -> %s", target_path, source_path)
target_path.symlink_to(source_path)
if host.is_wsl:
target_path = self.windows_home_dir / pathlib.PureWindowsPath(name)
log.debug(" target: %s", target_path)
if target_path.exists():
log.debug(" target path exists, will remove")
target_path.unlink()
log.debug(" copy: %s -> %s", source_path, target_path)
shutil.copy(source_path, target_path)
@property
def config_path(self):
# pylint: disable=missing-function-docstring
return self.options.config
@cached_property
def config(self):
"""
loads the json configuration
the contents of our configuration file
"""
with open(self.config_path, 'r', encoding='utf-8') as config_fp:
log.debug("loading config from path %s", self.config_path)
return json.load(config_fp)
@property
def prefs_dir(self):
"""
directory containing our preferences repo
"""
here = pathlib.Path(os.path.realpath(__file__))
return here.parent.parent
@cached_property
def windows_home_dir(self):
"""
Finds the home directory of the user's Windows home directory and
returns it as a Posix path representing its mount point from the
perspective of WSL.
"""
if not host.is_wsl:
raise Exception("cannot get windows home dir from anything other than wsl")
res = subprocess.run(['wslvar', 'USERPROFILE'], check=False,
capture_output=True)
winpath = res.stdout.decode('utf-8').strip()
res = subprocess.run(['wslpath', winpath], check=False,
capture_output=True)
return pathlib.Path(res.stdout.decode('utf-8').strip())
def setup_file(self, fname):
"""
sets up an individual file
"""
source = self.prefs_dir / fname
print(source)
def map_section(self, section_name):
section = self.options.config[section_name]
for source_name in section:
target_name = section[source_name]
source_path = pathlib.Path(source_name)
target_path = pathlib.Path(target_name)
print(f"Map {source_path} to {target_path}")
self.map_file(source_path, target_path)

@ -3,8 +3,11 @@ host module represents our host: the machine on which the installer script is
running. On WSL, that means we're on Linux
"""
import sys
import ctypes
import os
import platform
import sys
import pathlib
from functools import cached_property
class _Host:
@ -26,4 +29,30 @@ class _Host:
"""
return platform.system() == 'Linux'
@cached_property
def is_windows(self):
"""
true if we're on Windows (and running Python from Windows)
"""
return platform.system() == 'Windows'
@cached_property
def is_admin(self):
"""
tells us whether the running user has admin powers or not
"""
try:
return os.getuid() == 0
except AttributeError:
return ctypes.windll.shell32.IsUserAnAdmin() != 0
@property
def dotfiles_root(self):
"""
directory containing our preferences repo
"""
here = pathlib.Path(os.path.realpath(__file__))
return here.parent.parent
sys.modules[__name__] = _Host()

@ -3,8 +3,10 @@ cli options
"""
import argparse
import configparser
import pathlib
from installer import log
from installer import log, host
class Options:
"""
@ -24,10 +26,7 @@ class Options:
""")
parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-q', '--quiet', action='store_true')
parser.add_argument('-c', '--config', default='config.ini',
help="path to config file",
metavar='config.ini',
type=argparse.FileType('r', encoding='utf-8'))
parser.add_argument('-c', '--config', help="path to config file")
options = cls()
parser.parse_args(namespace=options)
@ -64,4 +63,12 @@ class Options:
@config.setter
def config(self, val):
self._config = val
if val is None:
path = host.dotfiles_root / "config.ini"
else:
path = pathlib.Path(val)
with open(path, 'r', encoding='utf-8') as config_file:
parser = configparser.ConfigParser()
parser.read_file(config_file)
self._config = parser

@ -1,3 +1,148 @@
"""
defines target classes: the places where things should be written
"""
import pathlib
import subprocess
import shutil
import os
from functools import cached_property
from installer import host
class Target:
"""
base class of all target platforms
"""
def target_path(self, relpath):
"""
computes the path of a file in the home directory
"""
return self.target_root / relpath
@cached_property
def target_root(self):
"""
locates the home directory
"""
return pathlib.Path.home()
def map_file(self, source_path, target_path):
"""
maps a file from a source path to some target path. The source path is
expected to be an absolute path, while the target_path is a relative
path, relative to the target's root.
"""
if not target_path.is_absolute():
target_path = self.target_root / target_path
print(f"target path: {target_path}")
print(f"target drive: {target_path.drive}")
if not target_path.parent.exists():
print("creating missing parent directories for target")
parent_dir = target_path.parent
parent_dir.mkdir(parents=True)
print(f"checking if target path {target_path.parts} exists")
exists = os.path.exists(str(target_path))
print(f"exists: {exists}")
is_link = os.path.islink(str(target_path))
print(f"is link: {is_link}")
if target_path.exists():
print("target path exists")
if target_path.is_symlink():
print("target path is symlink")
if target_path.resolve() == source_path:
print("symlink is up to date")
return
print("removing out of date symlink")
target_path.unlink()
elif target_path.is_file():
print("removing existing regular file")
target_path.unlink()
elif target_path.is_dir():
print("skip: target path is existing directory")
return
else:
print("skip: target path already exists")
return
else:
print("target path does not exist")
if target_path.is_symlink():
# ok this deserves some explaining: if a symlink points to
# itself, then pathlib considers it to not exist. It can't be
# resolved because it's an infinite loop, but a non-existent
# file that is a symlink is a circular reference in a symlink.
print("removing broken symlink")
target_path.unlink()
print("creating symlink")
target_path.symlink_to(source_path)
class Linux(Target):
"""
defines a local Linux target: the local machine when the script is run on
Linux
"""
class Windows(Target):
"""
defines a local Windows target: the local machine when the script is run on
Windows
"""
class WSLHost(Target):
"""
defines the Windows machine on which the WSL instance is hosted
"""
@cached_property
def target_root(self):
if not host.is_wsl:
raise Exception("cannot get windows home dir from anything other than wsl")
res = subprocess.run(['wslvar', 'USERPROFILE'], check=False,
capture_output=True)
winpath = res.stdout.decode('utf-8').strip()
res = subprocess.run(['wslpath', winpath], check=False,
capture_output=True)
return pathlib.Path(res.stdout.decode('utf-8').strip())
def map_file(self, source_path, target_path):
"""
maps a file from a source path to some target path. The source path is
expected to be an absolute path, while the target_path is a relative
path, relative to the target's root.
"""
if source_path.is_file():
clone = shutil.copy
elif source_path.is_dir():
clone = shutil.copytree
else:
print(f"source path {source_path} is not a file or directory")
return
if not target_path.is_absolute():
target_path = self.target_root / target_path
print(f"target path: {target_path}")
print(f"target drive: {target_path.drive}")
if not target_path.parent.exists():
print("creating missing parent directories for target")
parent_dir = target_path.parent
parent_dir.mkdir(parents=True)
print(f"checking if target path {target_path.parts} exists")
exists = os.path.exists(str(target_path))
print(f"exists: {exists}")
is_link = os.path.islink(str(target_path))
print(f"is link: {is_link}")
if target_path.exists():
print("target path exists")
if target_path.is_file():
print("removing existing regular file")
target_path.unlink()
else:
print("skip: target path exists and is not a regular file")
return
else:
print("target path does not exist")
print("copying file to target")
clone(source_path, target_path)

Loading…
Cancel
Save