-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP implementation of installing / compiling
- Loading branch information
1 parent
a8c5bc1
commit d787d42
Showing
4 changed files
with
362 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |