Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : add support for generating manpages for crc cli commands (#4181) #4586

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cmd/crc/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"strings"
"time"

"github.com/crc-org/crc/v2/pkg/crc/manpages"

"github.com/spf13/cobra/doc"

cmdBundle "github.com/crc-org/crc/v2/cmd/crc/cmd/bundle"
cmdConfig "github.com/crc-org/crc/v2/cmd/crc/cmd/config"
crcConfig "github.com/crc-org/crc/v2/pkg/crc/config"
Expand Down Expand Up @@ -64,7 +68,6 @@ func init() {
logging.Warn(err.Error())
logging.Warn("Error during segment client initialization, telemetry will be unavailable in this session")
}

// subcommands
rootCmd.AddCommand(cmdConfig.GetConfigCmd(config))
rootCmd.AddCommand(cmdBundle.GetBundleCmd(config))
Expand Down Expand Up @@ -103,6 +106,10 @@ const (
func Execute() {
attachMiddleware([]string{}, rootCmd)

if err := manpages.GenerateManPages(crcManPageGenerator, constants.CrcManPageDir); err != nil {
logging.Warn("Error generating man-pages")
}

if err := rootCmd.ExecuteContext(telemetry.NewContext(context.Background())); err != nil {
runPostrun()
_, _ = fmt.Fprintln(os.Stderr, err.Error())
Expand Down Expand Up @@ -186,3 +193,7 @@ func attachMiddleware(names []string, cmd *cobra.Command) {
cmd.RunE = executeWithLogging(fullCmd, src)
}
}

func crcManPageGenerator(targetDir string) error {
return doc.GenManTree(rootCmd, manpages.CrcManPageHeader, targetDir)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how much extra time does it take when we generate it first time?

Copy link
Contributor Author

@rohanKanojia rohanKanojia Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to measure execution time using the time command however I'm not seeing any significant difference in execution times. Execution time comes out different on every execution:

In both cases I ran crc cleanup before to make sure no man pages were generated.

Time for crc setup on v2.46.0:

real    0m4.171s
user    0m0.127s
sys     0m0.140s

Time for crc setup based on this PR:

real    0m3.906s
user    0m0.097s
sys     0m0.108s

46 changes: 46 additions & 0 deletions cmd/crc/cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cmd

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCrcManPageGenerator_WhenInvoked_GeneratesManPagesForAllCrcSubCommands(t *testing.T) {
// Given
dir := t.TempDir()

// When
err := crcManPageGenerator(dir)

// Then
assert.NoError(t, err)
files, readErr := os.ReadDir(dir)
assert.NoError(t, readErr)
var manPagesFiles []string
for _, manPage := range files {
manPagesFiles = append(manPagesFiles, manPage.Name())
}
assert.ElementsMatch(t, []string{
"crc-bundle-generate.1",
"crc-bundle.1",
"crc-cleanup.1",
"crc-config-get.1",
"crc-config-set.1",
"crc-config-unset.1",
"crc-config-view.1",
"crc-config.1",
"crc-console.1",
"crc-delete.1",
"crc-ip.1",
"crc-oc-env.1",
"crc-podman-env.1",
"crc-setup.1",
"crc-start.1",
"crc-status.1",
"crc-stop.1",
"crc-version.1",
"crc.1",
}, manPagesFiles)
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ require (
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.2.0 // indirect
github.com/containers/storage v1.55.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/creack/pty v1.1.18 // indirect
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
Expand Down Expand Up @@ -165,6 +166,7 @@ require (
github.com/qdm12/dns/v2 v2.0.0-rc6 // indirect
github.com/qdm12/gosettings v0.4.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ github.com/containers/storage v1.55.1/go.mod h1:28cB81IDk+y7ok60Of6u52RbCeBRucbF
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/crc-org/admin-helper v0.5.4 h1:Wq6wp6514MipPHHYdoL2VUyhUL9qh26wR1I3qPaVxf4=
github.com/crc-org/admin-helper v0.5.4/go.mod h1:sFkqIILzKrt62CH1bJn5PSBFSdhaCyMdz6BG37N3TBE=
Expand Down Expand Up @@ -350,6 +351,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
Expand Down
1 change: 1 addition & 0 deletions pkg/crc/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func GetDefaultBundle(preset crcpreset.Preset) string {

var (
CrcBaseDir = filepath.Join(GetHomeDir(), ".crc")
CrcManPageDir = filepath.Join(GetHomeDir(), ".local", "share", "man")
CrcBinDir = filepath.Join(CrcBaseDir, "bin")
CrcOcBinDir = filepath.Join(CrcBinDir, "oc")
CrcPodmanBinDir = filepath.Join(CrcBinDir, "podman")
Expand Down
184 changes: 184 additions & 0 deletions pkg/crc/manpages/manpages_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//go:build !windows
// +build !windows

package manpages

import (
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/spf13/cobra/doc"

"github.com/crc-org/crc/v2/pkg/crc/logging"
)

var (
rootCrcManPage = "crc.1.gz"
osEnvGetter = os.Getenv
osEnvSetter = os.Setenv
ManPathEnvironmentVariable = "MANPATH"
CrcManPageHeader = &doc.GenManHeader{
Title: "CRC",
Section: "1",
}
)

// GenerateManPages generates manual pages for user commands and places them
// in the specified target directory. It performs the following steps:
//
// 1. Checks if the man pages should be generated based on the target folder.
// 2. Creates the necessary directory structure if it does not exist.
// 3. Generates man pages in a temporary directory.
// 4. Compresses the generated man pages and moves them to the target folder.
// 5. Updates the MANPATH environment variable to include the target directory.
// 6. Cleans up the temporary directory.
//
// manPageGenerator: Function that generates man pages in the specified directory.
// targetDir: Directory where the generated man pages should be placed.
//
// Returns an error if any step in the process fails.
func GenerateManPages(manPageGenerator func(targetDir string) error, targetDir string) error {
manUserCommandTargetFolder := filepath.Join(targetDir, "man1")
if !manPagesAlreadyGenerated(manUserCommandTargetFolder) {
if _, err := os.Stat(manUserCommandTargetFolder); os.IsNotExist(err) {
err = os.MkdirAll(manUserCommandTargetFolder, 0755)
if err != nil {
logging.Errorf("error in creating dir for man pages: %s", err.Error())
}
}
temporaryManPagesDir, err := generateManPagesInTemporaryDirectory(manPageGenerator)
if err != nil {
return err
}
err = compressManPages(temporaryManPagesDir, manUserCommandTargetFolder)
if err != nil {
return fmt.Errorf("error in compressing man pages: %s", err.Error())
}
err = appendToManPathEnvironmentVariable(targetDir)
if err != nil {
return fmt.Errorf("error updating MANPATH environment variable: %s", err.Error())
}
err = os.RemoveAll(temporaryManPagesDir)
if err != nil {
return fmt.Errorf("error removing temporary man pages directory: %s", err.Error())
}
}
return nil
}

func appendToManPathEnvironmentVariable(folder string) error {
manPath := osEnvGetter(ManPathEnvironmentVariable)
if !manPathAlreadyContains(folder, manPath) {
if manPath == "" {
manPath = folder
} else {
manPath = fmt.Sprintf("%s%c%s", manPath, os.PathListSeparator, folder)
}
err := osEnvSetter(ManPathEnvironmentVariable, manPath)
if err != nil {
return err
}
}

return nil
}

func manPathAlreadyContains(manPathEnvVarValue string, folder string) bool {
manDirs := strings.Split(manPathEnvVarValue, string(os.PathListSeparator))
for _, manDir := range manDirs {
if manDir == folder {
return true
}
}
return false
}

func removeFromManPathEnvironmentVariable(manPathEnvVarValue string, folder string) error {
manDirs := strings.Split(manPathEnvVarValue, string(os.PathListSeparator))
var updatedManPathEnvVarValues []string
for _, manDir := range manDirs {
if manDir != folder {
updatedManPathEnvVarValues = append(updatedManPathEnvVarValues, manDir)
}
}
return osEnvSetter(ManPathEnvironmentVariable, strings.Join(updatedManPathEnvVarValues, string(os.PathListSeparator)))
}

func generateManPagesInTemporaryDirectory(manPageGenerator func(targetDir string) error) (string, error) {
tempDir, err := os.MkdirTemp("", "crc-manpages")
if err != nil {
return "", err
}
manPagesGenerationErr := manPageGenerator(tempDir)
if manPagesGenerationErr != nil {
return "", manPagesGenerationErr
}
logging.Debugf("Successfully generated manpages in %s", tempDir)
return tempDir, nil
}

func compressManPages(manPagesSourceFolder string, manPagesTargetFolder string) error {
return filepath.Walk(manPagesSourceFolder, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if info.IsDir() {
return nil
}

srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()

compressedFilePath := filepath.Join(manPagesTargetFolder, info.Name()+".gz")
compressedFile, err := os.Create(compressedFilePath)
if err != nil {
return err
}
defer compressedFile.Close()

gzipWriter := gzip.NewWriter(compressedFile)
defer gzipWriter.Close()

_, err = io.Copy(gzipWriter, srcFile)
if err != nil {
return err
}
return nil
})
}

func manPagesAlreadyGenerated(manPagesTargetFolder string) bool {
rootCrcManPageFilePath := filepath.Join(manPagesTargetFolder, rootCrcManPage)
if _, err := os.Stat(rootCrcManPageFilePath); os.IsNotExist(err) {
return false
}
return true
}

func RemoveCrcManPages(manPageDir string) error {
manUserCommandTargetFolder := filepath.Join(manPageDir, "man1")
err := filepath.Walk(manUserCommandTargetFolder, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Base(path)[:len("crc")] == "crc" {
err = os.Remove(path)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
return removeFromManPathEnvironmentVariable(osEnvGetter(ManPathEnvironmentVariable), manPageDir)
}
Loading