Skip to content

Commit

Permalink
WIP implementation of installing / compiling
Browse files Browse the repository at this point in the history
  • Loading branch information
evanpurkhiser committed Jul 18, 2018
1 parent a8c5bc1 commit d787d42
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 0 deletions.
62 changes: 62 additions & 0 deletions installer/compiler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package installer

import (
"io"
"os"

"go.evanpurkhiser.com/dots/config"
"go.evanpurkhiser.com/dots/resolver"
)

func OpenDotfile(dotfile *resolver.Dotfile, config config.SourceConfig) (io.ReadCloser, error) {
files := make([]*os.File, len(dotfile.Sources))

for i, source := range dotfile.Sources {
file, err := os.Open(config.SourcePath + separator + source.Path)
if err != nil {
return nil, err
}

files[i] = file
}

compiler := &dotfileCompiler{
dotfile: dotfile,
config: config,
files: files,
}

return compiler, nil
}

type dotfileCompiler struct {
dotfile *resolver.Dotfile
config config.SourceConfig
files []*os.File
}

// TODO: If we want we can make this thing do caching of compiled dotfiels if
// we expect to install them later
func (c *dotfileCompiler) Read(p []byte) (n int, err error) {
readers := []io.Reader{}

for _, file := range c.files {
readers = append(readers, file)
}

// TODO: Implement filtered reading
return io.MultiReader(readers...).Read(p)
}

func (c *dotfileCompiler) Close() error {
var err error

for _, file := range c.files {
closeErr := file.Close()
if closeErr != nil {
err = closeErr
}
}

return err
}
165 changes: 165 additions & 0 deletions installer/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package installer

import (
"os"
"sync"

"go.evanpurkhiser.com/dots/config"
"go.evanpurkhiser.com/dots/resolver"
)

const separator = string(os.PathSeparator)

// A PreparedDotfile represents a dotfile that has been "prepared" for
// installation by verifying it's contents against the existing dotfile, and
// checking various other flags that require knowledge of the existing dotfile.
type PreparedDotfile struct {
*resolver.Dotfile

// InstallPath is the full path to which the dotfile will be installed.
InstallPath string

// ContentsDiffer is a boolean flag representing that the compiled source
// differs from the currently installed dotfile.
ContentsDiffer bool

// SourceModesDiffer indicates that a compiled dotfile (one with multiple
// sources) does not have a consistent mode across all sources. In this
// case the lowest mode will be used.
SourceModesDiffer bool

// ModeDiffers represents the change in modes between the compiled source
// and the currently installed dotfile. Equal modes can be verified by
// calling ModeDiff.IsSame.
ModeDiff ModeDiff

// RemovedNull is a warning flag indicating that the removed dotfile does
// not exist in the install tree, though the dotfile is marked as removed.
RemovedNull bool

// OverwritesExisting is a warning flag that indicates that installing this
// dotfile is overwriting a dotfile that was not part of the lockfile.
OverwritesExisting bool

// PrepareError keeps track of errors while preparing the dotfile. Should
// this contain any errors, the PreparedDotfile is likely incomplete.
PrepareError error
}

// ModeDiff represents a change in file mode.
type ModeDiff struct {
Old os.FileMode
New os.FileMode
}

// IsSame returns a boolean value indicating if the modes are equal.
func (d ModeDiff) IsSame() bool {
return d.New == d.Old
}

// PreparedDotfiles is a list of prepared dotfiles.
type PreparedDotfiles []*PreparedDotfile

// PrepareDotfiles iterates all passed dotfiles and creates an associated
// PreparedDotfile, returning a list of all prepared dotfiles.
func PrepareDotfiles(dotfiles resolver.Dotfiles, config config.SourceConfig) PreparedDotfiles {
preparedDotfiles := make(PreparedDotfiles, len(dotfiles))

waitGroup := sync.WaitGroup{}
waitGroup.Add(len(dotfiles))

prepare := func(index int, dotfile *resolver.Dotfile) {
defer waitGroup.Done()

installPath := config.InstallPath + separator + dotfile.Path

prepared := PreparedDotfile{
Dotfile: dotfile,
InstallPath: installPath,
}
preparedDotfiles[index] = &prepared

targetStat, targetStatErr := os.Lstat(installPath)

exists := !os.IsNotExist(targetStatErr)

// If we're unable to stat our target installation file and the file
// exists there's likely a permissions issue.
if targetStatErr != nil && exists {
prepared.PrepareError = targetStatErr
return
}

// Nothing needs to be verified if the dotfile is simply being added
if dotfile.Added && !exists {
return
}

if dotfile.Added && exists {
prepared.OverwritesExisting = true
}

if dotfile.Removed && !exists {
prepared.RemovedNull = true
}

sourceInfo := make([]os.FileInfo, len(dotfile.Sources))

for i, source := range dotfile.Sources {
path := config.SourcePath + separator + source.Path

info, err := os.Lstat(path)
if err != nil {
prepared.PrepareError = err
return
}
sourceInfo[i] = info
}

sourceMode, tookLowest := flattenModes(sourceInfo)

prepared.ModeDiff = ModeDiff{
Old: targetStat.Mode(),
New: sourceMode,
}
prepared.SourceModesDiffer = tookLowest

// If we are dealing with a dotfile with a single source we can quickly
// determine modification based on differing sizes, otherwise we will
// have to compare the compiled sources to the installed file.
if len(dotfile.Sources) == 1 && targetStat.Size() != sourceInfo[0].Size() {
prepared.ContentsDiffer = true
return
}

// Compare source and currently instlled dotfile
source, err := OpenDotfile(dotfile, config)
if err != nil {
prepared.PrepareError = err
return
}
defer source.Close()

target, err := os.Open(installPath)
if err != nil {
prepared.PrepareError = err
return
}
defer target.Close()

filesAreSame, err := compareReaders(source, target)
if err != nil {
prepared.PrepareError = err
}

prepared.ContentsDiffer = !filesAreSame
}

for i, dotfile := range dotfiles {
go prepare(i, dotfile)
}

waitGroup.Wait()

return preparedDotfiles
}
61 changes: 61 additions & 0 deletions installer/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package installer

import (
"bytes"
"io"
"os"
)

// flattenModes takes a list of objects implementing the os.FileInfo interface
// and flattens the mode into a single FileMode. If any of the modes differ, it
// will flatten the mode to into the lowest (least permissive) mode and set the
// boolean return value to true.
func flattenModes(infos []os.FileInfo) (m os.FileMode, tookLowest bool) {
if len(infos) < 1 {
return 0, false
}

lowestMode := infos[0].Mode()

for _, stat := range infos[1:] {
mode := stat.Mode()

if mode != lowestMode {
tookLowest = true
}

if mode < lowestMode {
lowestMode = mode
}
}

return lowestMode, tookLowest
}

const chunkSize = 4096

// compareReaders compares two io.Readers for differences.
func compareReaders(file1, file2 io.Reader) (bool, error) {
b1 := make([]byte, chunkSize)
b2 := make([]byte, chunkSize)

for {
n1, err1 := file1.Read(b1)
n2, err2 := file2.Read(b2)

if err1 != nil && err1 != io.EOF {
return false, err1
}
if err2 != nil && err2 != io.EOF {
return false, err2
}

if err1 == io.EOF || err2 == io.EOF {
return err1 == err2, nil
}

if !bytes.Equal(b1[:n1], b2[:n2]) {
return false, nil
}
}
}
74 changes: 74 additions & 0 deletions installer/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package installer

import (
"os"
"testing"
"time"
)

type modeStub os.FileMode

func (m modeStub) Name() string { return "" }
func (m modeStub) Size() int64 { return 0 }
func (m modeStub) Mode() os.FileMode { return os.FileMode(m) }
func (m modeStub) ModTime() time.Time { return time.Time{} }
func (m modeStub) IsDir() bool { return false }
func (m modeStub) Sys() interface{} { return nil }

func TestFlattenModes(t *testing.T) {
testCases := []struct {
caseName string
modes []os.FileMode
expectedMode os.FileMode
shouldTakeLowest bool
}{
{
caseName: "All same permissions",
modes: []os.FileMode{
os.ModePerm & 0777,
os.ModePerm & 0777,
os.ModePerm & 0777,
},
expectedMode: os.ModePerm & 0777,
shouldTakeLowest: false,
},
{
caseName: "Differing permissions",
modes: []os.FileMode{
os.ModePerm & 0755,
os.ModePerm & 0644,
os.ModePerm & 0777,
},
expectedMode: os.ModePerm & 0644,
shouldTakeLowest: true,
},
{
caseName: "With extra file modes",
modes: []os.FileMode{
os.ModePerm&0777 | os.ModeDir,
os.ModePerm&0644 | os.ModeCharDevice,
os.ModePerm&0644 | os.ModeDir,
},
expectedMode: os.ModePerm&0644 | os.ModeCharDevice,
shouldTakeLowest: true,
},
}

for _, testCase := range testCases {
infos := make([]os.FileInfo, len(testCase.modes))

for i, mode := range testCase.modes {
infos[i] = modeStub(mode)
}

mode, tookLowest := flattenModes(infos)

if mode != testCase.expectedMode {
t.Errorf("Expected mode = %s; got mode = %s", mode, testCase.expectedMode)
}

if tookLowest != testCase.shouldTakeLowest {
t.Errorf("Expected tookLowest = %t", testCase.shouldTakeLowest)
}
}
}

0 comments on commit d787d42

Please sign in to comment.