From 55f35e11462db1513239a09703b0a0c1865eaeab Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Thu, 27 May 2021 01:40:53 +0100 Subject: [PATCH] chore(package): add dmgbuild helper package to execute dmgbuild --- pkg/dmgbuild/dmgbuild.go | 67 +++++ pkg/dmgbuild/icon_view.go | 85 +++++++ pkg/dmgbuild/license.go | 183 ++++++++++++++ pkg/dmgbuild/list_view.go | 154 ++++++++++++ pkg/dmgbuild/settings.go | 256 ++++++++++++++++++++ pkg/dmgbuild/settings_test.go | 444 ++++++++++++++++++++++++++++++++++ pkg/dmgbuild/window.go | 83 +++++++ 7 files changed, 1272 insertions(+) create mode 100644 pkg/dmgbuild/dmgbuild.go create mode 100644 pkg/dmgbuild/icon_view.go create mode 100644 pkg/dmgbuild/license.go create mode 100644 pkg/dmgbuild/list_view.go create mode 100644 pkg/dmgbuild/settings.go create mode 100644 pkg/dmgbuild/settings_test.go create mode 100644 pkg/dmgbuild/window.go diff --git a/pkg/dmgbuild/dmgbuild.go b/pkg/dmgbuild/dmgbuild.go new file mode 100644 index 0000000..3348512 --- /dev/null +++ b/pkg/dmgbuild/dmgbuild.go @@ -0,0 +1,67 @@ +package dmgbuild + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/hashicorp/go-hclog" +) + +func Build(ctx context.Context, settings *Settings) error { + logger := hclog.NewNullLogger() + if settings.Logger == nil { + logger = settings.Logger + } + + if !strings.HasSuffix(logger.Name(), "dmgbuild") { + logger = logger.Named("dmgbuild") + } + + if settings == nil { + return fmt.Errorf("no settings provided") + } + + _, err := os.Stat(settings.Filename) + if !os.IsNotExist(err) { + return fmt.Errorf("output dmg exists: %s", settings.Filename) + } + + baseCmd := settings.Command + if baseCmd == "" { + path, err2 := exec.LookPath("dmgbuild") + if err2 != nil { + return err2 + } + baseCmd = path + } + + file, err := settings.TempFile() + if err != nil { + return err + } + defer os.Remove(file) + + args := []string{"-s", file, settings.VolumeName, settings.Filename} + + if logger.IsDebug() { + content, err := os.ReadFile(file) + if err != nil { + return err + } + logger.Debug("using settings", file, string(content)) + logger.Debug("executing", "command", baseCmd, "args", args) + } + + cmd := exec.CommandContext(ctx, baseCmd, args...) + if settings.Stdout != nil { + cmd.Stdout = settings.Stdout + } + if settings.Stderr != nil { + cmd.Stderr = settings.Stderr + } + + return cmd.Run() +} diff --git a/pkg/dmgbuild/icon_view.go b/pkg/dmgbuild/icon_view.go new file mode 100644 index 0000000..931e4ed --- /dev/null +++ b/pkg/dmgbuild/icon_view.go @@ -0,0 +1,85 @@ +package dmgbuild + +import "fmt" + +type arrageOrder string + +//nolint:golint +var ( + NameOrder arrageOrder = "name" + DateModifiedOrder arrageOrder = "date-modified" + DateCreatedOrder arrageOrder = "date-created" + DateAddedOrder arrageOrder = "date-added" + DateLastOpenedOrder arrageOrder = "date-last-opened" + SizeOrder arrageOrder = "size" + KindOrder arrageOrder = "kind" + LabelOrder arrageOrder = "label" +) + +type labelPosition string + +//nolint:golint +var ( + LabelBottom labelPosition = "bottom" + LabelRight labelPosition = "right" +) + +type IconView struct { + ArrangeBy arrageOrder + GridOffsetX int + GridOffsetY int + GridSpacing float32 + ScrollPosX float32 + ScrollPosY float32 + LabelPosition labelPosition + IconSize float32 + TextSize float32 +} + +func NewIconView() IconView { + return IconView{ + GridOffsetX: 0, + GridOffsetY: 0, + GridSpacing: 100, + ScrollPosX: 0.0, + ScrollPosY: 0.0, + LabelPosition: LabelBottom, + IconSize: 128, + TextSize: 16, + } +} + +func (s *IconView) Render() []string { + r := []string{} + + if s.ArrangeBy != "" { + r = append(r, "arrange_by = "+pyStr(string(s.ArrangeBy))+"\n") + } + if s.GridOffsetX > 0 || s.GridOffsetY > 0 { + r = append(r, fmt.Sprintf( + "grid_offset = (%d, %d)\n", + s.GridOffsetX, s.GridOffsetY, + )) + } + if s.GridSpacing > 0 { + r = append(r, fmt.Sprintf("grid_spacing = %.2f\n", s.GridSpacing)) + } + if s.ScrollPosX > 0 || s.ScrollPosY > 0 { + r = append(r, fmt.Sprintf( + "scroll_position = (%.2f, %.2f)\n", + s.ScrollPosX, s.ScrollPosY, + )) + } + if s.LabelPosition != "" { + r = append(r, "label_position = "+pyStr(string(s.LabelPosition))+"\n") + } + + if s.IconSize > 0 { + r = append(r, fmt.Sprintf("icon_size = %.2f\n", s.IconSize)) + } + if s.TextSize > 0 { + r = append(r, fmt.Sprintf("text_size = %.2f\n", s.TextSize)) + } + + return r +} diff --git a/pkg/dmgbuild/license.go b/pkg/dmgbuild/license.go new file mode 100644 index 0000000..92f0499 --- /dev/null +++ b/pkg/dmgbuild/license.go @@ -0,0 +1,183 @@ +package dmgbuild + +import ( + "fmt" + "sort" + "strings" +) + +type locale string + +//nolint:golint +var ( + LocaleAfZA locale = "af_ZA" + LocaleAr locale = "ar" + LocaleBeBY locale = "be_BY" + LocaleBgBG locale = "bg_BG" + LocaleBn locale = "bn" + LocaleBo locale = "bo" + LocaleBr locale = "br" + LocaleCaES locale = "ca_ES" + LocaleCsCZ locale = "cs_CZ" + LocaleCy locale = "cy" + LocaleDaDK locale = "da_DK" + LocaleDeAT locale = "de_AT" + LocaleDeCH locale = "de_CH" + LocaleDeDE locale = "de_DE" + LocaleDzBT locale = "dz_BT" + LocaleElCY locale = "el_CY" + LocaleElGR locale = "el_GR" + LocaleEnAU locale = "en_AU" + LocaleEnCA locale = "en_CA" + LocaleEnGB locale = "en_GB" + LocaleEnIE locale = "en_IE" + LocaleEnSG locale = "en_SG" + LocaleEnUS locale = "en_US" + LocaleEo locale = "eo" + LocaleEs419 locale = "es_419" + LocaleEsES locale = "es_ES" + LocaleEtEE locale = "et_EE" + LocaleFaIR locale = "fa_IR" + LocaleFiFI locale = "fi_FI" + LocaleFoFO locale = "fo_FO" + LocaleFr001 locale = "fr_001" + LocaleFrBE locale = "fr_BE" + LocaleFrCA locale = "fr_CA" + LocaleFrCH locale = "fr_CH" + LocaleFrFR locale = "fr_FR" + LocaleGaLatgIE locale = "ga-Latg_IE" + LocaleGaIE locale = "ga_IE" + LocaleGd locale = "gd" + LocaleGrc locale = "grc" + LocaleGuIN locale = "gu_IN" + LocaleGv locale = "gv" + LocaleHeIL locale = "he_IL" + LocaleHiIN locale = "hi_IN" + LocaleHrHR locale = "hr_HR" + LocaleHuHU locale = "hu_HU" + LocaleHyAM locale = "hy_AM" + LocaleIsIS locale = "is_IS" + LocaleItCH locale = "it_CH" + LocaleItIT locale = "it_IT" + LocaleIuCA locale = "iu_CA" + LocaleJaJP locale = "ja_JP" + LocaleKaGE locale = "ka_GE" + LocaleKl locale = "kl" + LocaleKoKR locale = "ko_KR" + LocaleLtLT locale = "lt_LT" + LocaleLvLV locale = "lv_LV" + LocaleMkMK locale = "mk_MK" + LocaleMrIN locale = "mr_IN" + LocaleMtMT locale = "mt_MT" + LocaleNbNO locale = "nb_NO" + LocaleNeNP locale = "ne_NP" + LocaleNlBE locale = "nl_BE" + LocaleNlNL locale = "nl_NL" + LocaleNnNO locale = "nn_NO" + LocalePa locale = "pa" + LocalePlPL locale = "pl_PL" + LocalePtBR locale = "pt_BR" + LocalePtPT locale = "pt_PT" + LocaleRoRO locale = "ro_RO" + LocaleRuRU locale = "ru_RU" + LocaleSe locale = "se" + LocaleSkSK locale = "sk_SK" + LocaleSlSI locale = "sl_SI" + LocaleSrRS locale = "sr_RS" + LocaleSvSE locale = "sv_SE" + LocaleThTH locale = "th_TH" + LocaleToTO locale = "to_TO" + LocaleTrTR locale = "tr_TR" + LocaleUkUA locale = "uk_UA" + LocaleUrIN locale = "ur_IN" + LocaleUrPK locale = "ur_PK" + LocaleUzUZ locale = "uz_UZ" + LocaleViVN locale = "vi_VN" + LocaleZhCN locale = "zh_CN" + LocaleZhTW locale = "zh_TW" +) + +type Buttons struct { + LanguageName string + Agree string + Disagree string + Print string + Save string + Message string +} + +type License struct { + DefaultLanguage locale + Licenses map[locale]string + Buttons map[locale]Buttons +} + +func NewLicense() License { + return License{} +} + +func (s *License) Render() []string { + var l []string + + if s.DefaultLanguage != "" { + l = append(l, + "\"default-language\": "+pyStr(string(s.DefaultLanguage)), + ) + } + + if len(s.Licenses) > 0 { + var items []string + for k, v := range s.Licenses { + items = append(items, fmt.Sprintf( + "%s: %s", pyStr(string(k)), pyMStr(v), + )) + } + sort.SliceStable(items, func(i, j int) bool { + return items[i] < items[j] + }) + l = append(l, + "\"licenses\": {\n "+ + strings.Join(items, ",\n ")+ + "\n }", + ) + } + + if len(s.Buttons) > 0 { + var items []string + for k, v := range s.Buttons { + items = append(items, fmt.Sprintf( + "%s: (\n"+ + " %s,\n"+ + " %s,\n"+ + " %s,\n"+ + " %s,\n"+ + " %s,\n"+ + " %s\n"+ + " )", + pyStr(string(k)), + pyStr(v.LanguageName), + pyStr(v.Agree), + pyStr(v.Disagree), + pyStr(v.Print), + pyStr(v.Save), + pyStr(v.Message), + )) + } + sort.SliceStable(items, func(i, j int) bool { + return items[i] < items[j] + }) + l = append(l, + "\"buttons\": {\n "+ + strings.Join(items, ",\n ")+ + "\n }", + ) + } + + if len(l) == 0 { + return []string{} + } + + return []string{ + "license = {\n " + strings.Join(l, ",\n ") + "\n}\n", + } +} diff --git a/pkg/dmgbuild/list_view.go b/pkg/dmgbuild/list_view.go new file mode 100644 index 0000000..b3f5c01 --- /dev/null +++ b/pkg/dmgbuild/list_view.go @@ -0,0 +1,154 @@ +package dmgbuild + +import ( + "fmt" + "sort" + "strings" +) + +type listColumn string + +//nolint:golint +var ( + NameColumn listColumn = "name" + DateModifiedColumn listColumn = "date-modified" + DateCreatedColumn listColumn = "date-created" + DateAddedColumn listColumn = "date-added" + DateLastOpenedColumn listColumn = "date-last-opened" + SizeColumn listColumn = "size" + KindColumn listColumn = "kind" + LabelColumn listColumn = "label" + VersionColumn listColumn = "version" + CommentsColumn listColumn = "comments" +) + +type direction string + +//nolint:golint +var ( + Ascending direction = "ascending" + Descending direction = "descending" +) + +type ListView struct { + SortBy listColumn + ScrollPosX int + ScrollPosY int + IconSize float32 + TextSize float32 + UseRelativeDates bool + CalculateAllSizes bool + Columns []listColumn + ColumnWidths map[listColumn]int + ColumnSortDirections map[listColumn]direction +} + +func NewListView() ListView { + return ListView{ + SortBy: NameColumn, + IconSize: 16, + TextSize: 12, + UseRelativeDates: true, + Columns: []listColumn{ + NameColumn, + DateModifiedColumn, + SizeColumn, + KindColumn, + DateAddedColumn, + }, + ColumnWidths: map[listColumn]int{ + (NameColumn): 300, + (DateModifiedColumn): 181, + (DateCreatedColumn): 181, + (DateAddedColumn): 181, + (DateLastOpenedColumn): 181, + (SizeColumn): 97, + (KindColumn): 115, + (LabelColumn): 100, + (VersionColumn): 75, + (CommentsColumn): 300, + }, + ColumnSortDirections: map[listColumn]direction{ + (NameColumn): Ascending, + (DateModifiedColumn): Descending, + (DateCreatedColumn): Descending, + (DateAddedColumn): Descending, + (DateLastOpenedColumn): Descending, + (SizeColumn): Descending, + (KindColumn): Ascending, + (LabelColumn): Ascending, + (VersionColumn): Ascending, + (CommentsColumn): Ascending, + }, + } +} + +func (s *ListView) Render() []string { + r := []string{} + + if s.SortBy != "" { + r = append(r, "list_sort_by = "+pyStr(string(s.SortBy))+"\n") + } + if s.ScrollPosX > 0 || s.ScrollPosY > 0 { + r = append(r, fmt.Sprintf( + "list_scroll_position = (%d, %d)\n", + s.ScrollPosX, s.ScrollPosY, + )) + } + if s.IconSize > 0 { + r = append(r, fmt.Sprintf("list_icon_size = %.2f\n", s.IconSize)) + } + if s.TextSize > 0 { + r = append(r, fmt.Sprintf("list_text_size = %.2f\n", s.TextSize)) + } + r = append(r, "list_use_relative_dates = "+pyBool(s.UseRelativeDates)+"\n") + r = append( + r, "list_calculate_all_sizes = "+pyBool(s.CalculateAllSizes)+"\n", + ) + + if len(s.Columns) > 0 { + var cols []string + for _, col := range s.Columns { + cols = append(cols, pyStr(string(col))) + } + r = append(r, + "list_columns = [\n "+strings.Join(cols, ",\n ")+"\n]\n", + ) + } + + if len(s.ColumnWidths) > 0 { + var cols []string + for col, w := range s.ColumnWidths { + cols = append(cols, fmt.Sprintf( + "%s: %d", pyStr(string(col)), w, + )) + } + sort.SliceStable(cols, func(i, j int) bool { + return cols[i] < cols[j] + }) + r = append(r, + "list_column_widths = {\n "+ + strings.Join(cols, ",\n ")+ + "\n}\n", + ) + } + + if len(s.ColumnSortDirections) > 0 { + var cols []string + for col, direction := range s.ColumnSortDirections { + cols = append(cols, fmt.Sprintf( + "%s: %s", pyStr(string(col)), pyStr(string(direction)), + )) + } + sort.SliceStable(cols, func(i, j int) bool { + return cols[i] < cols[j] + }) + r = append(r, + "list_column_sort_directions = {\n "+ + strings.Join(cols, ",\n ")+ + "\n}\n", + ) + } + + return r +} diff --git a/pkg/dmgbuild/settings.go b/pkg/dmgbuild/settings.go new file mode 100644 index 0000000..5b8c899 --- /dev/null +++ b/pkg/dmgbuild/settings.go @@ -0,0 +1,256 @@ +package dmgbuild + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/go-hclog" +) + +type format string + +//nolint:golint +var ( + UDROFormat format = "UDRO" // Read-only + UDCOFormat format = "UDCO" // Compressed (ADC) + UDZOFormat format = "UDZO" // Compressed (gzip) + UDBZFormat format = "UDBZ" // Compressed (bzip2) + UFBIFormat format = "UFBI" // Entire device + IPODFormat format = "IPOD" // iPod image + UDxxFormat format = "UDxx" // UDIF stub + UDSBFormat format = "UDSB" // Sparse bundle + UDSPFormat format = "UDSP" // Sparse + UDRWFormat format = "UDRW" // Read/write + UDTOFormat format = "UDTO" // DVD/CD master + DC42Format format = "DC42" // Disk Copy 4.2 + RdWrFormat format = "RdWr" // NDIF read/write + RdxxFormat format = "Rdxx" // NDIF read-only + ROCoFormat format = "ROCo" // NDIF Compressed + RkenFormat format = "Rken" // NDIF Compressed (KenCode) +) + +type File struct { + Path string + PosX int + PosY int + Hidden bool + HideExtension bool +} + +type Symlink struct { + Name string + Target string + PosX int + PosY int + Hidden bool + HideExtension bool +} + +type Settings struct { + // Command can be set to a custom dmgbuild executable path. If not set, + // the first "dmgbuild" executable within PATH will be used. + Command string + + // Stdout will be set as STDOUT target for dmgbuild execution if not nil. + Stdout io.Writer + + // Stderr will be set as STDERR target for dmgbuild execution if not nil. + Stderr io.Writer + + // Logger allows logging details of dmbuild process. + Logger hclog.Logger + + // dmgbuild settings + Filename string + VolumeName string + Format format + Size string + CompressionLevel int + Files []*File + Symlinks []*Symlink + Icon string + BadgeIcon string + Window Window + IconView IconView + ListView ListView + License License +} + +func NewSettings() *Settings { + return &Settings{ + Format: UDZOFormat, + CompressionLevel: 9, + Window: NewWindow(), + IconView: NewIconView(), + ListView: NewListView(), + License: NewLicense(), + } +} + +//nolint:funlen,gocyclo +// Render returns a string slice where each string is a separate settings +// statement. +func (s *Settings) Render() ([]string, error) { + r := []string{ + "# -*- coding: utf-8 -*-\n", + "from __future__ import unicode_literals\n", + } + + if s.Filename != "" { + r = append(r, "filename = "+pyStr(s.Filename)+"\n") + } + if s.VolumeName != "" { + r = append(r, "volume_name = "+pyStr(s.VolumeName)+"\n") + } + if s.Format != "" { + r = append(r, "format = "+pyStr(string(s.Format))+"\n") + } + if s.CompressionLevel != 0 { + r = append(r, fmt.Sprintf( + "compression_level = %d\n", s.CompressionLevel, + )) + } + if s.Size != "" { + r = append(r, "size = "+pyStr(s.Size)+"\n") + } + + var files []string + var symlinks []string + var hide []string + var hideExt []string + var iconLoc []string + + if len(s.Files) > 0 { + for _, f := range s.Files { + files = append(files, pyStr(f.Path)) + name := filepath.Base(f.Path) + if f.PosX > 0 || f.PosY > 0 { + iconLoc = append(iconLoc, + fmt.Sprintf("%s: (%d, %d)", pyStr(name), f.PosX, f.PosY), + ) + } + if f.Hidden { + hide = append(hide, pyStr(filepath.Base(f.Path))) + } + if f.HideExtension { + hideExt = append(hideExt, pyStr(filepath.Base(f.Path))) + } + } + } + + if len(s.Symlinks) > 0 { + for _, l := range s.Symlinks { + symlinks = append(symlinks, pyStr(l.Name)+": "+pyStr(l.Target)) + if l.PosX > 0 || l.PosY > 0 { + iconLoc = append(iconLoc, + fmt.Sprintf("%s: (%d, %d)", pyStr(l.Name), l.PosX, l.PosY), + ) + } + if l.Hidden { + hide = append(hide, pyStr(l.Name)) + } + if l.HideExtension { + hideExt = append(hideExt, pyStr(l.Name)) + } + } + } + + if len(files) > 0 { + r = append(r, + "files = [\n "+strings.Join(files, ",\n ")+"\n]\n", + ) + } + if len(symlinks) > 0 { + r = append(r, + "symlinks = {\n "+strings.Join(symlinks, ",\n ")+"\n}\n", + ) + } + if len(hide) > 0 { + r = append(r, + "hide = [\n "+strings.Join(hide, ",\n ")+"\n]\n", + ) + } + if len(hideExt) > 0 { + r = append(r, + "hide_extensions = [\n "+strings.Join(hideExt, ",\n ")+ + "\n]\n", + ) + } + if len(iconLoc) > 0 { + r = append(r, + "icon_locations = {\n "+strings.Join(iconLoc, ",\n ")+"\n}\n", + ) + } + + if s.Icon != "" { + r = append(r, "icon = "+pyStr(s.Icon)+"\n") + } + if s.BadgeIcon != "" { + r = append(r, "badge_icon = "+pyStr(s.BadgeIcon)+"\n") + } + + r = append(r, s.Window.Render()...) + r = append(r, s.IconView.Render()...) + r = append(r, s.ListView.Render()...) + r = append(r, s.License.Render()...) + + return r, nil +} + +func (s *Settings) Write(w io.Writer) error { + out, err := s.Render() + if err != nil { + return err + } + + for _, o := range out { + _, err := w.Write([]byte(o)) + if err != nil { + return err + } + } + + return nil +} + +func (s *Settings) TempFile() (string, error) { + f, err := os.CreateTemp("", "*.dmgbuild.settings.py") + if err != nil { + return "", err + } + defer f.Close() + + err = s.Write(f) + if err != nil { + return "", err + } + + return f.Name(), nil +} + +func pyStr(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `"`, `\"`) + s = strings.ReplaceAll(s, "\r", `\r`) + s = strings.ReplaceAll(s, "\n", `\n`) + + return `"` + s + `"` +} + +func pyMStr(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `"`, `\"`) + + return `"""` + s + `"""` +} + +func pyBool(v bool) string { + if v { + return "True" + } + + return "False" +} diff --git a/pkg/dmgbuild/settings_test.go b/pkg/dmgbuild/settings_test.go new file mode 100644 index 0000000..e3ba25b --- /dev/null +++ b/pkg/dmgbuild/settings_test.go @@ -0,0 +1,444 @@ +package dmgbuild + +import ( + "bytes" + "strings" + "testing" + + "github.com/jimeh/undent" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSettings_Write(t *testing.T) { + test := []struct { + name string + entitlements *Settings + want string + }{ + { + name: "empty", + entitlements: &Settings{}, + want: undent.String(` + # -*- coding: utf-8 -*- + from __future__ import unicode_literals + show_status_bar = False + show_tab_view = False + show_toolbar = False + show_pathbar = False + show_sidebar = False + show_icon_preview = False + show_item_info = False + include_icon_view_settings = False + include_list_view_settings = False + list_use_relative_dates = False + list_calculate_all_sizes = False`, + ), + }, + { + name: "full", + entitlements: &Settings{ + Filename: "/builds/Emacs.2021-05-25.f4dc646.master.dmg", + VolumeName: "Emacs.2021-05-25.f4dc646.master", + Format: UDBZFormat, + CompressionLevel: 8, + Size: "100m", + Files: []*File{ + { + Path: "/builds/Emacs.app", + PosX: 200, + PosY: 200, + }, + { + Path: "/builds/README.rtf", + PosX: 200, + PosY: 300, + HideExtension: true, + }, + { + Path: "/builds/hide-me.png", + Hidden: true, + }, + }, + Symlinks: []*Symlink{ + { + Name: "Applications", + Target: "/Applications", + PosX: 400, + PosY: 400, + }, + { + Name: "QuickLook", + Target: "/Library/QuickLook", + PosX: 500, + PosY: 400, + Hidden: true, + }, + { + Name: "System", + Target: "/System", + HideExtension: true, + }, + }, + Icon: "/opt/misc/assets/volIcon.icns", + BadgeIcon: "/builds/Emacs.app/Contents/Resources/Icon.icns", + Window: Window{ + PoxX: 200, + PosY: 250, + Width: 680, + Height: 446, + Background: "/opt/misc/assets/bg.tif", + ShowStatusBar: true, + ShowTabView: true, + ShowToolbar: true, + ShowPathbar: true, + ShowSidebar: true, + SidebarWidth: 165, + DefaultView: list, + ShowIconPreview: true, + ShowItemInfo: true, + IncludeIconViewSettings: true, + IncludeListViewSettings: true, + }, + IconView: IconView{ + ArrangeBy: NameOrder, + GridOffsetX: 42, + GridOffsetY: 43, + GridSpacing: 44.5, + ScrollPosX: 4.5, + ScrollPosY: 5.5, + LabelPosition: LabelBottom, + IconSize: 160, + TextSize: 15, + }, + ListView: ListView{ + SortBy: NameColumn, + ScrollPosX: 7, + ScrollPosY: 8, + IconSize: 16, + TextSize: 12, + UseRelativeDates: true, + CalculateAllSizes: true, + Columns: []listColumn{ + NameColumn, + DateModifiedColumn, + DateCreatedColumn, + DateAddedColumn, + DateLastOpenedColumn, + SizeColumn, + KindColumn, + LabelColumn, + VersionColumn, + CommentsColumn, + }, + ColumnWidths: map[listColumn]int{ + (NameColumn): 300, + (DateModifiedColumn): 181, + (DateCreatedColumn): 181, + (DateAddedColumn): 181, + (DateLastOpenedColumn): 181, + (SizeColumn): 97, + (KindColumn): 115, + (LabelColumn): 100, + (VersionColumn): 75, + (CommentsColumn): 300, + }, + ColumnSortDirections: map[listColumn]direction{ + (NameColumn): Ascending, + (DateModifiedColumn): Descending, + (DateCreatedColumn): Descending, + (DateAddedColumn): Descending, + (DateLastOpenedColumn): Descending, + (SizeColumn): Descending, + (KindColumn): Ascending, + (LabelColumn): Ascending, + (VersionColumn): Ascending, + (CommentsColumn): Ascending, + }, + }, + License: License{ + DefaultLanguage: LocaleEnUS, + Licenses: map[locale]string{ + //nolint:lll + (LocaleEnGB): undent.String(` + {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf820 + {\fonttbl\f0\fnil\fcharset0 Helvetica-Bold;\f1\fnil\fcharset0 Helvetica;} + {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} + {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} + \paperw11905\paperh16837\margl1133\margr1133\margb1133\margt1133 + \deftab720 + \pard\pardeftab720\sa160\partightenfactor0 + \f0\b\fs60 \cf2 \expnd0\expndtw0\kerning0 + \up0 \nosupersub \ulnone \outl0\strokewidth0 \strokec2 Test License\ + \pard\pardeftab720\sa160\partightenfactor0 + \fs36 \cf2 \strokec2 What is this?\ + \pard\pardeftab720\sa160\partightenfactor0 + \f1\b0\fs22 \cf2 \strokec2 This is the English license. It says what you are allowed to do with this software.\ + \ + }`, + ), + //nolint:lll + (LocaleSe): undent.String(` + {\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf820 + {\fonttbl\f0\fnil\fcharset0 Helvetica-Bold;\f1\fnil\fcharset0 Helvetica;} + {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} + {\*\expandedcolortbl;;\cssrgb\c0\c0\c0;} + \paperw11905\paperh16837\margl1133\margr1133\margb1133\margt1133 + \deftab720 + \pard\pardeftab720\sa160\partightenfactor0 + \f0\b\fs60 \cf2 \expnd0\expndtw0\kerning0 + \up0 \nosupersub \ulnone \outl0\strokewidth0 \strokec2 Test License\ + \pard\pardeftab720\sa160\partightenfactor0 + \fs36 \cf2 \strokec2 What is this?\ + \pard\pardeftab720\sa160\partightenfactor0 + \f1\b0\fs22 \cf2 \strokec2 Detta är den engelska licensen. Det står vad du får göra med den här programvaran.\ + \ + }`, + ), + }, + Buttons: map[locale]Buttons{ + (LocaleEnGB): { + LanguageName: "English", + Agree: "Agree", + Disagree: "Disagree", + Print: "Print", + Save: "Save", + Message: "If you agree with the terms of this " + + "license, press \"Agree\" to install the " + + "software. If you do not agree, press " + + "\"Disagree\".", + }, + (LocaleSe): { + LanguageName: "Svenska", + Agree: "Godkänn", + Disagree: "Håller inte med", + Print: "Skriv ut", + Save: "Spara", + Message: "Om du godkänner villkoren i denna " + + "licens, tryck på \"Godkänn\" för att " + + "installera programvaran. Om du inte håller " + + "med, tryck på \"Håller inte med\".", + }, + }, + }, + }, + //nolint:lll + want: undent.String(` + # -*- coding: utf-8 -*- + from __future__ import unicode_literals + filename = "/builds/Emacs.2021-05-25.f4dc646.master.dmg" + volume_name = "Emacs.2021-05-25.f4dc646.master" + format = "UDBZ" + compression_level = 8 + size = "100m" + files = [ + "/builds/Emacs.app", + "/builds/README.rtf", + "/builds/hide-me.png" + ] + symlinks = { + "Applications": "/Applications", + "QuickLook": "/Library/QuickLook", + "System": "/System" + } + hide = [ + "hide-me.png", + "QuickLook" + ] + hide_extensions = [ + "README.rtf", + "System" + ] + icon_locations = { + "Emacs.app": (200, 200), + "README.rtf": (200, 300), + "Applications": (400, 400), + "QuickLook": (500, 400) + } + icon = "/opt/misc/assets/volIcon.icns" + badge_icon = "/builds/Emacs.app/Contents/Resources/Icon.icns" + background = "/opt/misc/assets/bg.tif" + show_status_bar = True + show_tab_view = True + show_toolbar = True + show_pathbar = True + show_sidebar = True + sidebar_width = 165 + default_view = "list-view" + window_rect = ((200, 250), (680, 446)) + show_icon_preview = True + show_item_info = True + include_icon_view_settings = True + include_list_view_settings = True + arrange_by = "name" + grid_offset = (42, 43) + grid_spacing = 44.50 + scroll_position = (4.50, 5.50) + label_position = "bottom" + icon_size = 160.00 + text_size = 15.00 + list_sort_by = "name" + list_scroll_position = (7, 8) + list_icon_size = 16.00 + list_text_size = 12.00 + list_use_relative_dates = True + list_calculate_all_sizes = True + list_columns = [ + "name", + "date-modified", + "date-created", + "date-added", + "date-last-opened", + "size", + "kind", + "label", + "version", + "comments" + ] + list_column_widths = { + "comments": 300, + "date-added": 181, + "date-created": 181, + "date-last-opened": 181, + "date-modified": 181, + "kind": 115, + "label": 100, + "name": 300, + "size": 97, + "version": 75 + } + list_column_sort_directions = { + "comments": "ascending", + "date-added": "descending", + "date-created": "descending", + "date-last-opened": "descending", + "date-modified": "descending", + "kind": "ascending", + "label": "ascending", + "name": "ascending", + "size": "descending", + "version": "ascending" + } + license = { + "default-language": "en_US", + "licenses": { + "en_GB": """{\\rtf1\\ansi\\ansicpg1252\\cocoartf1504\\cocoasubrtf820 + {\\fonttbl\\f0\\fnil\\fcharset0 Helvetica-Bold;\\f1\\fnil\\fcharset0 Helvetica;} + {\\colortbl;\\red255\\green255\\blue255;\\red0\\green0\\blue0;} + {\\*\\expandedcolortbl;;\\cssrgb\\c0\\c0\\c0;} + \\paperw11905\\paperh16837\\margl1133\\margr1133\\margb1133\\margt1133 + \\deftab720 + \\pard\\pardeftab720\\sa160\\partightenfactor0 + \\f0\\b\\fs60 \\cf2 \\expnd0\\expndtw0\\kerning0 + \\up0 \\nosupersub \\ulnone \\outl0\\strokewidth0 \\strokec2 Test License\\ + \\pard\\pardeftab720\\sa160\\partightenfactor0 + \\fs36 \\cf2 \\strokec2 What is this?\\ + \\pard\\pardeftab720\\sa160\\partightenfactor0 + \\f1\\b0\\fs22 \\cf2 \\strokec2 This is the English license. It says what you are allowed to do with this software.\\ + \\ + }""", + "se": """{\\rtf1\\ansi\\ansicpg1252\\cocoartf1504\\cocoasubrtf820 + {\\fonttbl\\f0\\fnil\\fcharset0 Helvetica-Bold;\\f1\\fnil\\fcharset0 Helvetica;} + {\\colortbl;\\red255\\green255\\blue255;\\red0\\green0\\blue0;} + {\\*\\expandedcolortbl;;\\cssrgb\\c0\\c0\\c0;} + \\paperw11905\\paperh16837\\margl1133\\margr1133\\margb1133\\margt1133 + \\deftab720 + \\pard\\pardeftab720\\sa160\\partightenfactor0 + \\f0\\b\\fs60 \\cf2 \\expnd0\\expndtw0\\kerning0 + \\up0 \\nosupersub \\ulnone \\outl0\\strokewidth0 \\strokec2 Test License\\ + \\pard\\pardeftab720\\sa160\\partightenfactor0 + \\fs36 \\cf2 \\strokec2 What is this?\\ + \\pard\\pardeftab720\\sa160\\partightenfactor0 + \\f1\\b0\\fs22 \\cf2 \\strokec2 Detta är den engelska licensen. Det står vad du får göra med den här programvaran.\\ + \\ + }""" + }, + "buttons": { + "en_GB": ( + "English", + "Agree", + "Disagree", + "Print", + "Save", + "If you agree with the terms of this license, press \"Agree\" to install the software. If you do not agree, press \"Disagree\"." + ), + "se": ( + "Svenska", + "Godkänn", + "Håller inte med", + "Skriv ut", + "Spara", + "Om du godkänner villkoren i denna licens, tryck på \"Godkänn\" för att installera programvaran. Om du inte håller med, tryck på \"Håller inte med\"." + ) + } + }`, + ), + }, + } + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + + err := tt.entitlements.Write(&buf) + require.NoError(t, err) + + assert.Equal(t, tt.want, strings.TrimSpace(buf.String())) + }) + } +} + +func Test_pyStr(t *testing.T) { + tests := []struct { + name string + s string + want string + }{ + { + name: "empty", + s: "", + want: `""`, + }, + { + name: "regular string", + s: "foo-bar nope :)", + want: `"foo-bar nope :)"`, + }, + { + name: "with single quotes", + s: "john's lost 'flip-flop'", + want: `"john's lost 'flip-flop'"`, + }, + { + name: "with double quotes", + s: `john has lost a "flip-flop"`, + want: `"john has lost a \"flip-flop\""`, + }, + { + name: "with backslashes", + s: `C:\path\to\file.txt`, + want: `"C:\\path\\to\\file.txt"`, + }, + { + name: "with line-feed", + s: "hello\nworld", + want: `"hello\nworld"`, + }, + { + name: "with carriage return", + s: "hello\rworld", + want: `"hello\rworld"`, + }, + { + name: "with backslashes, single and double quotes", + s: `john's "lost" C:\path\to\file.txt`, + want: `"john's \"lost\" C:\\path\\to\\file.txt"`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := pyStr(tt.s) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dmgbuild/window.go b/pkg/dmgbuild/window.go new file mode 100644 index 0000000..a9e96c3 --- /dev/null +++ b/pkg/dmgbuild/window.go @@ -0,0 +1,83 @@ +package dmgbuild + +import "fmt" + +type view string + +//nolint:golint +var ( + Icon view = "icon-view" + list view = "list-view" + Column view = "column-view" + Coverflow view = "coverflow" +) + +type Window struct { + PoxX int + PosY int + Width int + Height int + Background string + ShowStatusBar bool + ShowTabView bool + ShowToolbar bool + ShowPathbar bool + ShowSidebar bool + SidebarWidth int + DefaultView view + ShowIconPreview bool + ShowItemInfo bool + IncludeIconViewSettings bool + IncludeListViewSettings bool +} + +func NewWindow() Window { + return Window{ + PoxX: 100, + PosY: 150, + Width: 640, + Height: 280, + Background: "builtin-arrow", + DefaultView: Icon, + } +} + +func (s *Window) Render() []string { + r := []string{} + + if s.Background != "" { + r = append(r, "background = "+pyStr(s.Background)+"\n") + } + + r = append(r, "show_status_bar = "+pyBool(s.ShowStatusBar)+"\n") + r = append(r, "show_tab_view = "+pyBool(s.ShowTabView)+"\n") + r = append(r, "show_toolbar = "+pyBool(s.ShowToolbar)+"\n") + r = append(r, "show_pathbar = "+pyBool(s.ShowPathbar)+"\n") + r = append(r, "show_sidebar = "+pyBool(s.ShowSidebar)+"\n") + + if s.SidebarWidth > 0 { + r = append(r, fmt.Sprintf( + "sidebar_width = %d\n", s.SidebarWidth, + )) + } + if s.DefaultView != "" { + r = append(r, "default_view = "+pyStr(string(s.DefaultView))+"\n") + } + if s.Width > 0 && s.Height > 0 { + r = append(r, fmt.Sprintf( + "window_rect = ((%d, %d), (%d, %d))\n", + s.PoxX, s.PosY, s.Width, s.Height, + )) + } + + r = append(r, "show_icon_preview = "+pyBool(s.ShowIconPreview)+"\n") + r = append(r, "show_item_info = "+pyBool(s.ShowIconPreview)+"\n") + r = append( + r, "include_icon_view_settings = "+pyBool(s.ShowIconPreview)+"\n", + ) + r = append( + r, "include_list_view_settings = "+pyBool(s.ShowIconPreview)+"\n", + ) + + return r +}