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] [home]
dirs= files=
# sync vim plugin directories but not all of .vim
.vim/ftplugin .vim/ftplugin
.vim/pack .vim/pack
files= # individual files to sync
.bash_profile .bash_profile
.bashrc .bashrc
.screenrc .screenrc
@ -11,3 +14,13 @@ files=
.vimrc .vimrc
.config/htop/htoprc .config/htop/htoprc
.config/lazydocker/config.yml .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 installs preferences
""" """
from installer import install from installer import Installer
install() installer = Installer()
installer.run()

@ -14,13 +14,7 @@ from functools import cached_property
from installer import host from installer import host
from installer.options import Options from installer.options import Options
from installer import log from installer import log
from installer import targets
def install():
"""
runs our install process based on cli arguments
"""
installer = Installer()
installer.run()
class Installer: class Installer:
""" """
@ -28,86 +22,75 @@ class Installer:
""" """
def __init__(self): def __init__(self):
self.options = Options.from_cli_args() self.options = Options.from_cli_args()
self.config_path = "include.json"
def run(self): def run(self):
""" """
runs the install process runs the install process
""" """
if home_files := self.config.get('home_files'): if host.is_windows and not host.is_admin:
for name in home_files: print("You are not admin: admin is required on Windows")
self.install_home_file(name) 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 print(f"source path: {source_path}")
log.debug(" source: %s", source_path) print(f"source drive: {source_path.drive}")
if not source_path.exists(): if not source_path.exists():
log.warning("home file source path %s does not exist", source_path) print('skip: no such file')
return return
if not source_path.is_file(): for target in self.targets:
log.warning("home file source path %s is not a file", source_path) target.map_file(source_path, target_path)
return
if host.is_linux: @property
target_path = pathlib.Path.home() / name def config_path(self):
log.debug(" target: %s", target_path) # pylint: disable=missing-function-docstring
if target_path.exists(): return self.options.config
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)
@cached_property @cached_property
def config(self): 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: with open(self.config_path, 'r', encoding='utf-8') as config_fp:
log.debug("loading config from path %s", self.config_path) log.debug("loading config from path %s", self.config_path)
return json.load(config_fp) return json.load(config_fp)
@property def map_section(self, section_name):
def prefs_dir(self): section = self.options.config[section_name]
""" for source_name in section:
directory containing our preferences repo target_name = section[source_name]
""" source_path = pathlib.Path(source_name)
here = pathlib.Path(os.path.realpath(__file__)) target_path = pathlib.Path(target_name)
return here.parent.parent print(f"Map {source_path} to {target_path}")
self.map_file(source_path, target_path)
@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)

@ -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 running. On WSL, that means we're on Linux
""" """
import sys import ctypes
import os
import platform import platform
import sys
import pathlib
from functools import cached_property from functools import cached_property
class _Host: class _Host:
@ -26,4 +29,30 @@ class _Host:
""" """
return platform.system() == 'Linux' 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() sys.modules[__name__] = _Host()

@ -3,8 +3,10 @@ cli options
""" """
import argparse import argparse
import configparser
import pathlib
from installer import log from installer import log, host
class Options: class Options:
""" """
@ -24,10 +26,7 @@ class Options:
""") """)
parser.add_argument('-v', '--verbose', action='store_true') parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-q', '--quiet', action='store_true') parser.add_argument('-q', '--quiet', action='store_true')
parser.add_argument('-c', '--config', default='config.ini', parser.add_argument('-c', '--config', help="path to config file")
help="path to config file",
metavar='config.ini',
type=argparse.FileType('r', encoding='utf-8'))
options = cls() options = cls()
parser.parse_args(namespace=options) parser.parse_args(namespace=options)
@ -64,4 +63,12 @@ class Options:
@config.setter @config.setter
def config(self, val): 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 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