diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..581f392
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,10 @@
+builds:
+ - goos:
+ - windows
+ - darwin
+ - linux
+archive:
+ format: binary
+
+snapshot:
+ name_template: SNAPSHOT
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..97626ba
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..0a76216
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..a1105b8
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app-builder.iml b/app-builder.iml
new file mode 100644
index 0000000..eacc75a
--- /dev/null
+++ b/app-builder.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/commands/icon.go b/commands/icon.go
new file mode 100644
index 0000000..7d4e033
--- /dev/null
+++ b/commands/icon.go
@@ -0,0 +1,214 @@
+package commands
+
+import (
+ "image"
+ "os"
+
+ "github.com/disintegration/imaging"
+ "image/png"
+ "bufio"
+ "text/template"
+ "bytes"
+ "sync"
+ "runtime"
+ "os/exec"
+ "io/ioutil"
+ "path"
+ "encoding/json"
+ "fmt"
+ "github.com/develar/app-builder/util"
+ "strings"
+)
+
+type IconInfo struct {
+ File string `json:"file"`
+ Size int `json:"size"`
+}
+
+type Icns2PngMapping struct {
+ Id string
+ Size int
+}
+
+var icns2PngMappingList = []Icns2PngMapping{
+ {"is32", 16},
+ {"il32", 32},
+ {"ih32", 48},
+ {"icp6", 64},
+ {"it32", 128},
+ {"ic08", 256},
+ {"ic09", 512},
+}
+
+type ConvertIcnsToPngResult struct {
+ MaxIconPath string `json:"maxIconPath"`
+ Icons []IconInfo `json:"icons"`
+}
+
+func ConvertIcnsToPng(inFile string) error {
+ tempDir, err := util.TempDir(os.Getenv("ELECTRON_BUILDER_TMP_DIR"), ".iconset")
+ if err != nil {
+ return err
+ }
+
+ var maxIconPath string
+ var result []IconInfo
+
+ sizeList := []int{24, 96}
+ outFileTemplate := path.Join(tempDir, "icon_{{.Width}}x{{.Height}}.png")
+ if runtime.GOOS == "darwin" && os.Getenv("FORCE_ICNS2PNG") == "" {
+ output, err := exec.Command("iconutil", "--convert", "iconset", "--output", tempDir, inFile).CombinedOutput()
+ if err != nil {
+ fmt.Println(string(output))
+ return err
+ }
+
+ iconFiles, err := ioutil.ReadDir(tempDir)
+ if err != nil {
+ return err
+ }
+
+ for _, item := range icns2PngMappingList {
+ fileName := fmt.Sprintf("icon_%dx%d.png", item.Size, item.Size)
+ if contains(iconFiles, fileName) {
+ // list sorted by size, so, last assignment is a max size
+ maxIconPath = path.Join(tempDir, fileName)
+ result = append(result, IconInfo{maxIconPath, item.Size})
+ } else {
+ sizeList = append(sizeList, item.Size)
+ }
+ }
+ } else {
+ outputBytes, err := exec.Command("icns2png", "--extract", "--output", tempDir, inFile).CombinedOutput()
+ output := string(outputBytes)
+ if err != nil {
+ fmt.Println(output)
+ return err
+ }
+
+ namePrefix := strings.TrimSuffix(path.Base(inFile), path.Ext(inFile))
+
+ for _, item := range icns2PngMappingList {
+ if strings.Contains(output, item.Id) {
+ // list sorted by size, so, last assignment is a max size
+ maxIconPath = path.Join(tempDir, fmt.Sprintf("%s_%dx%dx32.png", namePrefix, item.Size, item.Size))
+ result = append(result, IconInfo{maxIconPath, item.Size})
+ } else {
+ sizeList = append(sizeList, item.Size)
+ }
+ }
+ }
+
+ err = multiResizeImage(maxIconPath, outFileTemplate, &result, sizeList, nil)
+ if err != nil {
+ return err
+ }
+
+ serializedResult, err := json.Marshal(ConvertIcnsToPngResult{
+ MaxIconPath: maxIconPath,
+ Icons: result,
+ })
+ if err != nil {
+ return err
+ }
+
+ _, err = os.Stdout.Write(serializedResult)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func contains(files []os.FileInfo, name string) bool {
+ for _, fileInfo := range files {
+ if fileInfo.Name() == name {
+ return true
+ }
+ }
+ return false
+}
+
+func multiResizeImage(inFile string, outFileNameTemplateString string, result *[]IconInfo, wList []int, hList []int) (error) {
+ reader, err := os.Open(inFile)
+ if err != nil {
+ return err
+ }
+
+ originalImage, _, err := image.Decode(reader)
+ if err != nil {
+ return err
+ }
+
+ if hList == nil || len(hList) == 0 {
+ hList = wList
+ }
+
+ outFileNameTemplate := template.Must(template.New("name").Parse(outFileNameTemplateString))
+
+ var waitGroup sync.WaitGroup
+
+ imageCount := len(wList)
+ waitGroup.Add(imageCount)
+
+ for i := 0; i < imageCount; i++ {
+ w := wList[i]
+ h := hList[i]
+
+ outFilePath, err := computeName(outFileNameTemplate, map[string]interface{}{
+ "Width": w,
+ "Height": h,
+ })
+ if err != nil {
+ return err
+ }
+
+ *result = append(*result, IconInfo{
+ File: outFilePath,
+ Size: w,
+ })
+ go resizeImage(originalImage, w, h, outFilePath, &waitGroup)
+ }
+
+ waitGroup.Wait()
+ return nil
+}
+
+func computeName(template *template.Template, data interface{}) (string, error) {
+ outFileNameBuffer := &bytes.Buffer{}
+ err := template.Execute(outFileNameBuffer, data)
+ if err != nil {
+ return "", err
+ }
+ return outFileNameBuffer.String(), nil
+}
+
+func resizeImage(originalImage image.Image, w int, h int, outFileName string, waitGroup *sync.WaitGroup) error {
+ defer waitGroup.Done()
+ newImage := imaging.Resize(originalImage, w, h, imaging.Lanczos)
+ return saveImage(newImage, outFileName)
+}
+
+func saveImage(image *image.NRGBA, outFileName string) error {
+ outFile, err := os.Create(outFileName)
+ if err != nil {
+ return err
+ }
+
+ writer := bufio.NewWriter(outFile)
+ err = png.Encode(writer, image)
+ if err != nil {
+ return err
+ }
+
+ flushError := writer.Flush()
+ closeError := outFile.Close()
+ if flushError != nil {
+ return flushError
+ }
+ if closeError != nil {
+ return closeError
+ }
+
+ return nil
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..b55c25e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ "gopkg.in/alecthomas/kingpin.v2"
+ "os"
+ "github.com/develar/app-builder/commands"
+ "log"
+)
+
+var (
+ app = kingpin.New("app-builder", "app-builder").Version("0.1.0")
+
+ icnsToPng = app.Command("icns-to-png", "convert ICNS to PNG")
+ icnsToPngInFile = icnsToPng.Flag("input", "input ICNS file").Short('i').Required().String()
+)
+
+func main() {
+ switch kingpin.MustParse(app.Parse(os.Args[1:])) {
+ case icnsToPng.FullCommand():
+ err := commands.ConvertIcnsToPng(*icnsToPngInFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+}
diff --git a/publish.sh b/publish.sh
new file mode 100755
index 0000000..4458809
--- /dev/null
+++ b/publish.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+set -e
+
+BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+if [ -z "$GITHUB_TOKEN" ] ; then
+ SEC=`security find-generic-password -l GH_TOKEN -g 2>&1`
+ export GITHUB_TOKEN=`echo "$SEC" | grep "password" | cut -d \" -f 2`
+fi
+
+NAME=app-builder
+VERSION=0.1.0
+
+OUT_DIR="$BASEDIR/dist/out"
+rm -rf "$OUT_DIR"
+
+publish()
+{
+ outDir=$1
+ archiveName="$NAME-v$VERSION-$2"
+ archiveFile="$OUT_DIR/$archiveName.7z"
+
+ cd "$BASEDIR/dist/$outDir"
+ 7za a -mx=9 -mfb=64 "$archiveFile" .
+}
+
+publish "darwinamd64" mac
+publish "linux386" linux-ia32
+publish "linuxamd64" linux-x64
+publish "windows386" win-ia32
+publish "windowsamd64" win-x64
+
+tool-releaser develar/app-builder "v$VERSION" master "" "$OUT_DIR/*.7z"
\ No newline at end of file
diff --git a/release.sh b/release.sh
new file mode 100755
index 0000000..7926669
--- /dev/null
+++ b/release.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+goreleaser --rm-dist "$@"
\ No newline at end of file
diff --git a/util/tempfile.go b/util/tempfile.go
new file mode 100644
index 0000000..8ea4ed2
--- /dev/null
+++ b/util/tempfile.go
@@ -0,0 +1,73 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package util
+
+import (
+ "os"
+ "path/filepath"
+ "strconv"
+ "sync"
+ "time"
+)
+
+// Random number state.
+// We generate random temporary file names so that there's a good
+// chance the file doesn't exist yet - keeps the number of tries in
+// TempFile to a minimum.
+var rand uint32
+var randmu sync.Mutex
+
+func reseed() uint32 {
+ return uint32(time.Now().UnixNano() + int64(os.Getpid()))
+}
+
+func nextPrefix() string {
+ randmu.Lock()
+ r := rand
+ if r == 0 {
+ r = reseed()
+ }
+ r = r*1664525 + 1013904223 // constants from Numerical Recipes
+ rand = r
+ randmu.Unlock()
+ return strconv.Itoa(int(1e9 + r%1e9))[1:]
+}
+
+// TempDir creates a new temporary directory in the directory dir
+// with a name beginning with prefix and returns the path of the
+// new directory. If dir is the empty string, TempDir uses the
+// default directory for temporary files (see os.TempDir).
+// Multiple programs calling TempDir simultaneously
+// will not choose the same directory. It is the caller's responsibility
+// to remove the directory when no longer needed.
+func TempDir(dir, suffix string) (name string, err error) {
+ if dir == "" {
+ dir = os.TempDir()
+ }
+
+ nConflict := 0
+ for i := 0; i < 10000; i++ {
+ try := filepath.Join(dir, nextPrefix() + suffix)
+ err = os.Mkdir(try, 0700)
+ if os.IsExist(err) {
+ if nConflict++; nConflict > 10 {
+ randmu.Lock()
+ rand = reseed()
+ randmu.Unlock()
+ }
+ continue
+ }
+ if os.IsNotExist(err) {
+ if _, err := os.Stat(dir); os.IsNotExist(err) {
+ return "", err
+ }
+ }
+ if err == nil {
+ name = try
+ }
+ break
+ }
+ return
+}