diff --git a/Earthfile b/Earthfile index 72504d8e..f5a24139 100644 --- a/Earthfile +++ b/Earthfile @@ -3,7 +3,7 @@ VERSION --arg-scope-and-set --global-cache 0.8 ARG --global go_version = 1.22 ARG --global node_version = 18 ARG --global rust_version = 1.79 -ARG --global tauri_version = "2.0.0-rc.8" +ARG --global tauri_version = "2.0.1" ARG --global golangci_lint_version = 1.57.1 ARG --global go_builder_image = "golang:${go_version}-alpine" @@ -509,7 +509,65 @@ tauri-lint: WORKDIR /app/desktop/tauri/src-tauri RUN --mount=$EARTHLY_RUST_TARGET_CACHE cargo clippy --all-targets --all-features -- -D warnings -tauri-bundle-linux: +release-prep: + FROM +rust-base + + # Linux specific + COPY (+tauri-build/output/portmaster --target="x86_64-unknown-linux-gnu") ./output/binary/linux_amd64/portmaster + COPY (+go-build/output/portmaster-core --GOARCH=amd64 --GOOS=linux --CMDS=portmaster-core) ./output/binary/linux_amd64/portmaster-core + + # Windows specific + COPY (+tauri-build/output/portmaster.exe --target="x86_64-pc-windows-gnu") ./output/binary/windows_amd64/portmaster.exe + COPY (+go-build/output/portmaster-core.exe --GOARCH=amd64 --GOOS=windows --CMDS=portmaster-core) ./output/binary/windows_amd64/portmaster-core.exe + # TODO(vladimir): figure out a way to get the lastest release of the kext. + RUN touch ./output/binary/windows_amd64/portmaster-kext.sys + + # All platforms + COPY (+assets/assets.zip) ./output/binary/all/assets.zip + COPY (+angular-project/output/portmaster.zip --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster/) ./output/binary/all/portmaster.zip + + # Intel + # TODO(vladimir): figure out a way to download all latest intel data. + RUN mkdir -p ./output/intel + RUN wget -O ./output/intel/geoipv4.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv4_v20240529-0-1.mmdb.gz" && \ + wget -O ./output/intel/geoipv6.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv6_v20240529-0-1.mmdb.gz" + + RUN touch "./output/intel/index.dsd" + RUN touch "./output/intel/base.dsdl" + RUN touch "./output/intel/intermediate.dsdl" + RUN touch "./output/intel/urgent.dsdl" + + COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr + RUN ./updatemgr -dir "./output/binary" -name "Binary" > ./output/binary/bin-index.json + RUN ./updatemgr -dir "./output/intel" -name "Intel" > ./output/intel/intel-index.json + + # Intel Extracted (needed for the installers) + RUN mkdir -p ./output/intel_decompressed + RUN cp ./output/intel/intel-index.json ./output/intel_decompressed/intel-index.json + RUN gzip -dc ./output/intel/geoipv4.mmdb.gz > ./output/intel_decompressed/geoipv4.mmdb + RUN gzip -dc ./output/intel/geoipv6.mmdb.gz > ./output/intel_decompressed/geoipv6.mmdb + RUN touch "./output/intel_decompressed/index.dsd" + RUN touch "./output/intel_decompressed/base.dsdl" + RUN touch "./output/intel_decompressed/intermediate.dsdl" + RUN touch "./output/intel_decompressed/urgent.dsdl" + + # Save all artifacts to output folder + SAVE ARTIFACT --if-exists --keep-ts "output/binary/bin-index.json" AS LOCAL "${outputDir}/binary/bin-index.json" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/all/*" AS LOCAL "${outputDir}/binary/all/" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/linux_amd64/*" AS LOCAL "${outputDir}/binary/linux_amd64/" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/windows_amd64/*" AS LOCAL "${outputDir}/binary/windows_amd64/" + SAVE ARTIFACT --if-exists --keep-ts "output/intel/*" AS LOCAL "${outputDir}/intel/" + SAVE ARTIFACT --if-exists --keep-ts "output/intel_decompressed/*" AS LOCAL "${outputDir}/intel_decompressed/" + + # Save all artifacts to the container output folder so other containers can access it. + SAVE ARTIFACT --if-exists --keep-ts "output/binary/bin-index.json" "output/binary/bin-index.json" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/all/*" "output/binary/all/" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/linux_amd64/*" "output/binary/linux_amd64/" + SAVE ARTIFACT --if-exists --keep-ts "output/binary/windows_amd64/*" "output/binary/windows_amd64/" + SAVE ARTIFACT --if-exists --keep-ts "output/intel/*" "output/intel/" + SAVE ARTIFACT --if-exists --keep-ts "output/intel_decompressed/*" "output/intel_decompressed/" + +installer-linux: FROM +rust-base # ARG --required target ARG target="x86_64-unknown-linux-gnu" @@ -523,42 +581,21 @@ tauri-bundle-linux: SAVE IMAGE --cache-hint - DO +RUST_TO_GO_ARCH_STRING --rustTarget="${target}" # Build and copy the binaries RUN mkdir -p target/${target}/release - COPY (+tauri-build/output/portmaster --target=x86_64-unknown-linux-gnu) ./target/${target}/release/portmaster - + COPY (+release-prep/output/binary/linux_amd64/portmaster) ./target/${target}/release/portmaster RUN mkdir -p binary - COPY (+go-build/output/portmaster-core --GOARCH=amd64 --GOOS=linux --CMDS=portmaster-core) ./binary/portmaster-core - - COPY (+assets/assets.zip) ./binary/assets.zip - COPY (+angular-project/output/portmaster.zip --project=portmaster --dist=./dist --configuration=production --baseHref=/ui/modules/portmaster/) ./binary/portmaster.zip - + COPY (+release-prep/output/binary/bin-index.json) ./binary/bin-index.json + COPY (+release-prep/output/binary/linux_amd64/portmaster-core) ./binary/portmaster-core + COPY (+release-prep/output/binary/all/portmaster.zip) ./binary/portmaster.zip + COPY (+release-prep/output/binary/all/assets.zip) ./binary/assets.zip # Download the intel data RUN mkdir -p intel - - RUN wget -O ./intel/geoipv4.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv4_v20240529-0-1.mmdb.gz" && \ - wget -O ./intel/geoipv6.mmdb.gz "https://updates.safing.io/all/intel/geoip/geoipv6_v20240529-0-1.mmdb.gz" && \ - gzip -d ./intel/geoipv4.mmdb.gz && \ - gzip -d ./intel/geoipv6.mmdb.gz - - RUN touch "./intel/index.dsd" - RUN touch "./intel/base.dsdl" - RUN touch "./intel/intermediate.dsdl" - RUN touch "./intel/urgent.dsdl" - - - # Generate index files - COPY (+go-build/output/updatemgr --GOARCH=amd64 --GOOS=linux --CMDS=updatemgr) ./updatemgr - RUN ./updatemgr -dir "./binary" -name "Binary" > ./binary/bin-index.json - RUN ./updatemgr -dir "./intel" -name "Intel" > ./intel/intel-index.json - - RUN cat ./binary/bin-index.json - RUN cat ./intel/intel-index.json + COPY (+release-prep/output/intel_decompressed/*) ./intel/ # build the installers RUN cargo tauri bundle --ci --target="${target}" diff --git a/cmds/updatemgr/main.go b/cmds/updatemgr/main.go index 519d94eb..a157af07 100644 --- a/cmds/updatemgr/main.go +++ b/cmds/updatemgr/main.go @@ -10,17 +10,21 @@ import ( ) var binaryMap = map[string]updates.Artifact{ - "portmaster-core": { - Platform: "linux_amd64", + "geoipv4.mmdb.gz": { + Filename: "geoipv4.mmdb", + Unpack: "gz", }, - "portmaster-core.exe": { - Platform: "windows_amd64", - }, - "portmaster-kext.sys": { - Platform: "windows_amd64", + "geoipv6.mmdb.gz": { + Filename: "geoipv6.mmdb", + Unpack: "gz", }, } +var ignoreFiles = map[string]struct{}{ + "bin-index.json": {}, + "intel-index.json": {}, +} + func main() { dir := flag.String("dir", "", "path to the directory that contains the artifacts") name := flag.String("name", "", "name of the bundle") @@ -36,7 +40,13 @@ func main() { return } - bundle, err := updates.GenerateBundleFromDir(*name, *version, binaryMap, *dir) + settings := updates.BundleFileSettings{ + Name: *name, + Version: *version, + Properties: binaryMap, + IgnoreFiles: ignoreFiles, + } + bundle, err := updates.GenerateBundleFromDir(*dir, settings) if err != nil { fmt.Fprintf(os.Stderr, "failed to generate bundle: %s\n", err) return diff --git a/desktop/tauri/src-tauri/Cargo.toml b/desktop/tauri/src-tauri/Cargo.toml index f38c9679..78f87926 100644 --- a/desktop/tauri/src-tauri/Cargo.toml +++ b/desktop/tauri/src-tauri/Cargo.toml @@ -12,21 +12,21 @@ rust-version = "1.64" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] -tauri-build = { version = "2.0.0-rc.7", features = [] } +tauri-build = { version = "2.0.1", features = [] } [dependencies] # Tauri -tauri = { version = "2.0.0-rc.8", features = ["tray-icon", "image-png", "config-json5", "devtools"] } -tauri-plugin-shell = "2.0.0-rc" -tauri-plugin-dialog = "2.0.0-rc" -tauri-plugin-clipboard-manager = "2.0.0-rc" -tauri-plugin-os = "2.0.0-rc" -tauri-plugin-single-instance = "2.0.0-rc" -tauri-plugin-notification = "2.0.0-rc" -tauri-plugin-log = "2.0.0-rc" -tauri-plugin-window-state = "2.0.0-rc" +tauri = { version = "2.0.1", features = ["tray-icon", "image-png", "config-json5", "devtools"] } +tauri-plugin-shell = "2.0.1" +tauri-plugin-dialog = "2.0.1" +tauri-plugin-clipboard-manager = "2.0.1" +tauri-plugin-os = "2.0.1" +tauri-plugin-single-instance = "2.0.1" +tauri-plugin-notification = "2.0.1" +tauri-plugin-log = "2.0.1" +tauri-plugin-window-state = "2.0.1" -tauri-cli = "2.0.0-rc.8" +tauri-cli = "2.0.1" clap_lex = "0.7.2" # General diff --git a/desktop/tauri/src-tauri/templates/main.wxs b/desktop/tauri/src-tauri/templates/main.wxs index f42ba777..f62f27a3 100644 --- a/desktop/tauri/src-tauri/templates/main.wxs +++ b/desktop/tauri/src-tauri/templates/main.wxs @@ -139,7 +139,7 @@ {{/each~}} - + {{#each file_associations as |association| ~}} {{#each association.ext as |ext| ~}} diff --git a/desktop/tauri/src-tauri/templates/main_original.wxs b/desktop/tauri/src-tauri/templates/main_original.wxs index 1b8116ed..b1d2672a 100644 --- a/desktop/tauri/src-tauri/templates/main_original.wxs +++ b/desktop/tauri/src-tauri/templates/main_original.wxs @@ -139,7 +139,7 @@ {{/each~}} - + {{#each file_associations as |association| ~}} {{#each association.ext as |ext| ~}} diff --git a/packaging/windows/generate_windows_installers.ps1 b/packaging/windows/generate_windows_installers.ps1 new file mode 100644 index 00000000..ffa03cec --- /dev/null +++ b/packaging/windows/generate_windows_installers.ps1 @@ -0,0 +1,46 @@ +# Save the current directory +$originalDirectory = Get-Location + +$destinationDir = "desktop/tauri/src-tauri" +$binaryDir = "$destinationDir/binary" +$intelDir = "$destinationDir/intel" + +# Make sure distination folder exists. +if (-not (Test-Path -Path $binaryDir)) { + New-Item -ItemType Directory -Path $binaryDir > $null +} + +# Copy binary files +Copy-Item -Force -Path "dist/binary/bin-index.json" -Destination "$binaryDir/bin-index.json" +Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-core.exe" -Destination "$binaryDir/portmaster-core.exe" +Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster-kext.sys" -Destination "$binaryDir/portmaster-kext.sys" +Copy-Item -Force -Path "dist/binary/all/portmaster.zip" -Destination "$binaryDir/portmaster.zip" +Copy-Item -Force -Path "dist/binary/all/assets.zip" -Destination "$binaryDir/assets.zip" +Copy-Item -Force -Path "dist/binary/windows_amd64/portmaster.exe" -Destination "$destinationDir/target/release/portmaster.exe" + +# Make sure distination folder exists. +if (-not (Test-Path -Path $intelDir)) { + New-Item -ItemType Directory -Path $intelDir > $null +} +# Copy intel data +Copy-Item -Force -Path "dist/intel_decompressed/*" -Destination "$intelDir/" + +Set-Location $destinationDir + +# Download tauri-cli +Invoke-WebRequest -Uri https://github.com/tauri-apps/tauri/releases/download/tauri-cli-v2.0.1/cargo-tauri-x86_64-pc-windows-msvc.zip -OutFile tauri-cli.zip +Expand-Archive -Force tauri-cli.zip + +./tauri-cli/cargo-tauri.exe bundle + +$installerDist = "..\..\..\dist\windows_amd64\" +# Make sure distination folder exists. +if (-not (Test-Path -Path $installerDist)) { + New-Item -ItemType Directory -Path $installerDist > $null +} + +Copy-Item -Path ".\target\release\bundle\msi\*" -Destination $installerDist +Copy-Item -Path ".\target\release\bundle\nsis\*" -Destination $installerDist + +# Restore the original directory +Set-Location $originalDirectory \ No newline at end of file diff --git a/service/updates/bundle.go b/service/updates/bundle.go index 6cd2d430..02b6089c 100644 --- a/service/updates/bundle.go +++ b/service/updates/bundle.go @@ -120,57 +120,3 @@ func checkIfFileIsValid(filename string, artifact Artifact) (bool, error) { } return true, nil } - -// GenerateBundleFromDir generates a bundle from a given folder. -func GenerateBundleFromDir(name string, version string, properties map[string]Artifact, dirPath string) (*Bundle, error) { - files, err := os.ReadDir(dirPath) - if err != nil { - return nil, err - } - artifacts := make([]Artifact, 0, len(files)) - for _, f := range files { - // Skip dirs - if f.IsDir() { - continue - } - - artifact := Artifact{ - Filename: f.Name(), - } - predefined, ok := properties[f.Name()] - // Check if caller supplied predefined settings for this artifact. - if ok { - // File that have compression may have different filename and artifact filename. (because of the extension) - // If caller did not specify the artifact filename set it as the same as the filename. - if predefined.Filename == "" { - predefined.Filename = f.Name() - } - artifact = predefined - } - content, err := os.ReadFile(filepath.Join(dirPath, f.Name())) - if err != nil { - return nil, err - } - - // Decompress if compression was applied to the file. - if artifact.Unpack != "" { - content, err = unpack(artifact.Unpack, content) - if err != nil { - return nil, err - } - } - - hash := sha256.Sum256(content) - hashStr := hex.EncodeToString(hash[:]) - artifact.SHA256 = hashStr - - artifacts = append(artifacts, artifact) - } - - return &Bundle{ - Name: name, - Version: version, - Artifacts: artifacts, - Published: time.Now(), - }, nil -} diff --git a/service/updates/bundlegeneration.go b/service/updates/bundlegeneration.go new file mode 100644 index 00000000..12ac8b20 --- /dev/null +++ b/service/updates/bundlegeneration.go @@ -0,0 +1,184 @@ +package updates + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "time" + + semver "github.com/hashicorp/go-version" +) + +type BundleFileSettings struct { + Name string + Version string + Properties map[string]Artifact + IgnoreFiles map[string]struct{} +} + +// GenerateBundleFromDir generates a bundle from a given folder. +func GenerateBundleFromDir(bundleDir string, settings BundleFileSettings) (*Bundle, error) { + bundleDirName := filepath.Base(bundleDir) + + artifacts := make([]Artifact, 0, 5) + err := filepath.Walk(bundleDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + // Skip folders + if info.IsDir() { + return nil + } + + identifier, version, ok := getIdentifierAndVersion(info.Name()) + if !ok { + identifier = info.Name() + } + + // Check if file is in the ignore list. + if _, ok := settings.IgnoreFiles[identifier]; ok { + return nil + } + + artifact := Artifact{} + + // Check if the caller provided properties for the artifact. + if p, ok := settings.Properties[identifier]; ok { + artifact = p + } + + // Set filename of artifact if not set by the caller. + if artifact.Filename == "" { + artifact.Filename = identifier + } + + artifact.Version = version + + // Fill the platform of the artifact + parentDir := filepath.Base(filepath.Dir(path)) + if parentDir != "all" && parentDir != bundleDirName { + artifact.Platform = parentDir + } + + // Fill the hash + hash, err := getSHA256(path, artifact.Unpack) + if err != nil { + return fmt.Errorf("failed to calculate hash of file: %s %w", path, err) + } + artifact.SHA256 = hash + + artifacts = append(artifacts, artifact) + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to walk the dir: %w", err) + } + + // Filter artifact so we have single version for each file + artifacts, err = selectLatestArtifacts(artifacts) + if err != nil { + return nil, fmt.Errorf("failed to select artifact version: %w", err) + } + + return &Bundle{ + Name: settings.Name, + Version: settings.Version, + Artifacts: artifacts, + Published: time.Now(), + }, nil +} + +func selectLatestArtifacts(artifacts []Artifact) ([]Artifact, error) { + artifactsMap := make(map[string]Artifact) + + for _, a := range artifacts { + // Make the key platform specific since there can be same filename for multiple platforms. + key := a.Filename + a.Platform + aMap, ok := artifactsMap[key] + if !ok { + artifactsMap[key] = a + continue + } + + if aMap.Version == "" || a.Version == "" { + return nil, fmt.Errorf("invalid mix version and non versioned files for: %s", a.Filename) + } + + mapVersion, err := semver.NewVersion(aMap.Version) + if err != nil { + return nil, fmt.Errorf("invalid version for artifact: %s", aMap.Filename) + } + + artifactVersion, err := semver.NewVersion(a.Version) + if err != nil { + return nil, fmt.Errorf("invalid version for artifact: %s", a.Filename) + } + + if mapVersion.LessThan(artifactVersion) { + artifactsMap[key] = a + } + } + + artifactsFiltered := make([]Artifact, 0, len(artifactsMap)) + for _, a := range artifactsMap { + artifactsFiltered = append(artifactsFiltered, a) + } + + return artifactsFiltered, nil +} + +func getSHA256(path string, unpackType string) (string, error) { + content, err := os.ReadFile(path) + if err != nil { + return "", err + } + + // Decompress if compression was applied to the file. + if unpackType != "" { + content, err = unpack(unpackType, content) + if err != nil { + return "", err + } + } + + // Calculate hash + hash := sha256.Sum256(content) + return hex.EncodeToString(hash[:]), nil +} + +var ( + fileVersionRegex = regexp.MustCompile(`_v[0-9]+-[0-9]+-[0-9]+(-[a-z]+)?`) + rawVersionRegex = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(-[a-z]+)?$`) +) + +func getIdentifierAndVersion(versionedPath string) (identifier, version string, ok bool) { + dirPath, filename := path.Split(versionedPath) + + // Extract version from filename. + rawVersion := fileVersionRegex.FindString(filename) + if rawVersion == "" { + // No version present in file, making it invalid. + return "", "", false + } + + // Trim the `_v` that gets caught by the regex and + // replace `-` with `.` to get the version string. + version = strings.Replace(strings.TrimLeft(rawVersion, "_v"), "-", ".", 2) + + // Put the filename back together without version. + i := strings.Index(filename, rawVersion) + if i < 0 { + // extracted version not in string (impossible) + return "", "", false + } + filename = filename[:i] + filename[i+len(rawVersion):] + + // Put the full path back together and return it. + // `dirPath + filename` is guaranteed by path.Split() + return dirPath + filename, version, true +}