Skip to content

Commit b8237d3

Browse files
committed
Implement basic installer
1 parent d787d42 commit b8237d3

File tree

6 files changed

+288
-149
lines changed

6 files changed

+288
-149
lines changed

installer/compiler.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import (
88
"go.evanpurkhiser.com/dots/resolver"
99
)
1010

11+
// mustCompile indicates if a dotfile msut be compiled, or if a single-source
12+
// dotfile does not require any transformations and may be directly installed.
13+
func shouldCompile(dotfile *resolver.Dotfile, config config.SourceConfig) bool {
14+
return len(dotfile.Sources) > 1
15+
}
16+
17+
// OpenDotfile opens a source dotfile for streaming compilation.
1118
func OpenDotfile(dotfile *resolver.Dotfile, config config.SourceConfig) (io.ReadCloser, error) {
1219
files := make([]*os.File, len(dotfile.Sources))
1320

installer/installer.go

Lines changed: 61 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,98 @@
11
package installer
22

33
import (
4+
"fmt"
5+
"io"
46
"os"
7+
"path"
58
"sync"
69

710
"go.evanpurkhiser.com/dots/config"
8-
"go.evanpurkhiser.com/dots/resolver"
911
)
1012

1113
const separator = string(os.PathSeparator)
1214

13-
// A PreparedDotfile represents a dotfile that has been "prepared" for
14-
// installation by verifying it's contents against the existing dotfile, and
15-
// checking various other flags that require knowledge of the existing dotfile.
16-
type PreparedDotfile struct {
17-
*resolver.Dotfile
15+
// directoryMode is the mode used to create directories for installed dotfiles.
16+
const directoryMode = 0755
1817

19-
// InstallPath is the full path to which the dotfile will be installed.
20-
InstallPath string
18+
// InstallConfig represents configuration options available for installing
19+
// a single or set of dotfiles.
20+
type InstallConfig struct {
21+
SourceConfig *config.SourceConfig
2122

22-
// ContentsDiffer is a boolean flag representing that the compiled source
23-
// differs from the currently installed dotfile.
24-
ContentsDiffer bool
25-
26-
// SourceModesDiffer indicates that a compiled dotfile (one with multiple
27-
// sources) does not have a consistent mode across all sources. In this
28-
// case the lowest mode will be used.
29-
SourceModesDiffer bool
30-
31-
// ModeDiffers represents the change in modes between the compiled source
32-
// and the currently installed dotfile. Equal modes can be verified by
33-
// calling ModeDiff.IsSame.
34-
ModeDiff ModeDiff
35-
36-
// RemovedNull is a warning flag indicating that the removed dotfile does
37-
// not exist in the install tree, though the dotfile is marked as removed.
38-
RemovedNull bool
39-
40-
// OverwritesExisting is a warning flag that indicates that installing this
41-
// dotfile is overwriting a dotfile that was not part of the lockfile.
42-
OverwritesExisting bool
43-
44-
// PrepareError keeps track of errors while preparing the dotfile. Should
45-
// this contain any errors, the PreparedDotfile is likely incomplete.
46-
PrepareError error
47-
}
48-
49-
// ModeDiff represents a change in file mode.
50-
type ModeDiff struct {
51-
Old os.FileMode
52-
New os.FileMode
23+
// OverrideInstallPath specifies a path to install the dotfile at,
24+
// overriding the configuration in the SourceConfig.
25+
OverrideInstallPath string
5326
}
5427

55-
// IsSame returns a boolean value indicating if the modes are equal.
56-
func (d ModeDiff) IsSame() bool {
57-
return d.New == d.Old
58-
}
28+
func InstallDotfile(dotfile *PreparedDotfile, config InstallConfig) error {
29+
installPath := config.SourceConfig.InstallPath + separator + dotfile.Path
5930

60-
// PreparedDotfiles is a list of prepared dotfiles.
61-
type PreparedDotfiles []*PreparedDotfile
31+
if config.OverrideInstallPath != "" {
32+
installPath = config.OverrideInstallPath + separator + dotfile.Path
33+
}
6234

63-
// PrepareDotfiles iterates all passed dotfiles and creates an associated
64-
// PreparedDotfile, returning a list of all prepared dotfiles.
65-
func PrepareDotfiles(dotfiles resolver.Dotfiles, config config.SourceConfig) PreparedDotfiles {
66-
preparedDotfiles := make(PreparedDotfiles, len(dotfiles))
35+
if dotfile.SourcesAreIrregular {
36+
return fmt.Errorf("Source files are not all regular files")
37+
}
6738

68-
waitGroup := sync.WaitGroup{}
69-
waitGroup.Add(len(dotfiles))
39+
// No change
40+
if !dotfile.IsNew && !dotfile.ContentsDiffer && dotfile.Permissions.IsSame() {
41+
return nil
42+
}
7043

71-
prepare := func(index int, dotfile *resolver.Dotfile) {
72-
defer waitGroup.Done()
44+
// Removed
45+
if dotfile.Removed && !dotfile.RemovedNull {
46+
return os.Remove(installPath)
47+
}
7348

74-
installPath := config.InstallPath + separator + dotfile.Path
49+
targetMode := dotfile.Permissions.New | dotfile.Mode
7550

76-
prepared := PreparedDotfile{
77-
Dotfile: dotfile,
78-
InstallPath: installPath,
79-
}
80-
preparedDotfiles[index] = &prepared
51+
// Only permissions differ
52+
if !dotfile.IsNew && !dotfile.Permissions.IsSame() && !dotfile.ContentsDiffer {
53+
return os.Chmod(installPath, targetMode)
54+
}
8155

82-
targetStat, targetStatErr := os.Lstat(installPath)
56+
if err := os.MkdirAll(path.Dir(installPath), directoryMode); err != nil {
57+
return err
58+
}
8359

84-
exists := !os.IsNotExist(targetStatErr)
60+
targetOpts := os.O_CREATE | os.O_TRUNC | os.O_WRONLY
8561

86-
// If we're unable to stat our target installation file and the file
87-
// exists there's likely a permissions issue.
88-
if targetStatErr != nil && exists {
89-
prepared.PrepareError = targetStatErr
90-
return
91-
}
62+
target, err := os.OpenFile(installPath, targetOpts, targetMode)
63+
if err != nil {
64+
return err
65+
}
66+
defer target.Close()
9267

93-
// Nothing needs to be verified if the dotfile is simply being added
94-
if dotfile.Added && !exists {
95-
return
96-
}
68+
source, err := OpenDotfile(dotfile.Dotfile, *config.SourceConfig)
69+
if err != nil {
70+
return err
71+
}
72+
defer source.Close()
9773

98-
if dotfile.Added && exists {
99-
prepared.OverwritesExisting = true
100-
}
74+
_, err = io.Copy(target, source)
10175

102-
if dotfile.Removed && !exists {
103-
prepared.RemovedNull = true
104-
}
76+
return err
77+
}
10578

106-
sourceInfo := make([]os.FileInfo, len(dotfile.Sources))
79+
func InstallDotfiles(dotfiles PreparedDotfiles, config InstallConfig) []error {
80+
waitGroup := sync.WaitGroup{}
81+
waitGroup.Add(len(dotfiles))
10782

108-
for i, source := range dotfile.Sources {
109-
path := config.SourcePath + separator + source.Path
83+
errors := []error{}
11084

111-
info, err := os.Lstat(path)
85+
for _, dotfile := range dotfiles {
86+
go func(dotfile *PreparedDotfile) {
87+
err := InstallDotfile(dotfile, config)
11288
if err != nil {
113-
prepared.PrepareError = err
114-
return
89+
errors = append(errors, err)
11590
}
116-
sourceInfo[i] = info
117-
}
118-
119-
sourceMode, tookLowest := flattenModes(sourceInfo)
120-
121-
prepared.ModeDiff = ModeDiff{
122-
Old: targetStat.Mode(),
123-
New: sourceMode,
124-
}
125-
prepared.SourceModesDiffer = tookLowest
126-
127-
// If we are dealing with a dotfile with a single source we can quickly
128-
// determine modification based on differing sizes, otherwise we will
129-
// have to compare the compiled sources to the installed file.
130-
if len(dotfile.Sources) == 1 && targetStat.Size() != sourceInfo[0].Size() {
131-
prepared.ContentsDiffer = true
132-
return
133-
}
134-
135-
// Compare source and currently instlled dotfile
136-
source, err := OpenDotfile(dotfile, config)
137-
if err != nil {
138-
prepared.PrepareError = err
139-
return
140-
}
141-
defer source.Close()
142-
143-
target, err := os.Open(installPath)
144-
if err != nil {
145-
prepared.PrepareError = err
146-
return
147-
}
148-
defer target.Close()
149-
150-
filesAreSame, err := compareReaders(source, target)
151-
if err != nil {
152-
prepared.PrepareError = err
153-
}
154-
155-
prepared.ContentsDiffer = !filesAreSame
156-
}
157-
158-
for i, dotfile := range dotfiles {
159-
go prepare(i, dotfile)
91+
waitGroup.Done()
92+
}(dotfile)
16093
}
16194

16295
waitGroup.Wait()
16396

164-
return preparedDotfiles
97+
return errors
16598
}

0 commit comments

Comments
 (0)