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

Support other java archive files #1931

Merged
merged 6 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
105 changes: 68 additions & 37 deletions cli/azd/pkg/project/framework_service_maven.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
)

// The default, conventional App Service Java package name
const AppServiceJavaPackageName = "app.jar"
const AppServiceJavaPackageName = "app"

type mavenProject struct {
env *environment.Environment
Expand Down Expand Up @@ -116,66 +116,97 @@ func (m *mavenProject) Package(
return
}

packageSource := buildOutput.BuildOutputPath
if packageSource == "" {
packageSource = serviceConfig.Path()
packageSrcPath := buildOutput.BuildOutputPath
if packageSrcPath == "" {
packageSrcPath = serviceConfig.Path()
}

if serviceConfig.OutputPath != "" {
packageSource = filepath.Join(packageSource, serviceConfig.OutputPath)
packageSrcPath = filepath.Join(packageSrcPath, serviceConfig.OutputPath)
} else {
packageSource = filepath.Join(packageSource, "target")
packageSrcPath = filepath.Join(packageSrcPath, "target")
}

entries, err := os.ReadDir(packageSource)
packageSrcFileInfo, err := os.Stat(packageSrcPath)
if err != nil {
task.SetError(fmt.Errorf("discovering JAR files in %s: %w", packageSource, err))
if serviceConfig.OutputPath == "" {
task.SetError(fmt.Errorf("reading default maven target path %s: %w", packageSrcPath, err))
} else {
task.SetError(fmt.Errorf("reading dist path %s: %w", packageSrcPath, err))
}
return
}

matches := []string{}
for _, entry := range entries {
if entry.IsDir() {
continue
archive := ""
if packageSrcFileInfo.IsDir() {
archive, err = m.discoverArchive(packageSrcPath)
if err != nil {
task.SetError(err)
return
}

if name := entry.Name(); strings.HasSuffix(name, ".jar") {
matches = append(matches, name)
} else {
archive = packageSrcPath
if !isSupportedJavaArchive(archive) {
ext := filepath.Ext(archive)
task.SetError(
fmt.Errorf(
//nolint:lll
"file %s with extension %s is not a supported java archive file (.ear, .war, .jar)", ext, archive))
return
}
}

if len(matches) == 0 {
task.SetError(fmt.Errorf("no JAR files found in %s", packageSource))
return
}
if len(matches) > 1 {
names := strings.Join(matches, ", ")
task.SetError(fmt.Errorf(
"multiple JAR files found in %s: %s. Only a single runnable JAR file is expected",
packageSource,
names,
))
return
}

packageSource = filepath.Join(packageSource, matches[0])

task.SetProgress(NewServiceProgress("Copying deployment package"))
err = copy.Copy(packageSource, filepath.Join(packageDest, AppServiceJavaPackageName))
ext := strings.ToLower(filepath.Ext(archive))
err = copy.Copy(archive, filepath.Join(packageDest, AppServiceJavaPackageName+ext))
if err != nil {
task.SetError(fmt.Errorf("copying to staging directory failed: %w", err))
return
}

if err := validatePackageOutput(packageDest); err != nil {
task.SetError(err)
return
}

task.SetResult(&ServicePackageResult{
Build: buildOutput,
PackagePath: packageDest,
})
},
)
}

func isSupportedJavaArchive(archiveFile string) bool {
ext := strings.ToLower(filepath.Ext(archiveFile))
return ext == ".jar" || ext == ".war" || ext == ".ear"
}

func (m *mavenProject) discoverArchive(dir string) (string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return "", fmt.Errorf("discovering java archive files in %s: %w", dir, err)
}

archiveFiles := []string{}
for _, entry := range entries {
if entry.IsDir() {
continue
}

name := entry.Name()
if isSupportedJavaArchive(name) {
archiveFiles = append(archiveFiles, name)
}
}

switch len(archiveFiles) {
case 0:
return "", fmt.Errorf("no java archive files (.jar, .ear, .war) found in %s", dir)
case 1:
return filepath.Join(dir, archiveFiles[0]), nil
default:
names := strings.Join(archiveFiles, ", ")
return "", fmt.Errorf(
//nolint:lll
"multiple java archive files (.jar, .ear, .war) found in %s: %s. To pick a specific archive to be used, specify the relative path to the archive file using the 'dist' property in azure.yaml",
weikanglim marked this conversation as resolved.
Show resolved Hide resolved
dir,
names,
)
}
}
165 changes: 165 additions & 0 deletions cli/azd/pkg/project/framework_service_maven_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/ext"
"github.com/azure/azure-dev/cli/azd/pkg/osutil"
"github.com/azure/azure-dev/cli/azd/pkg/tools/javac"
"github.com/azure/azure-dev/cli/azd/pkg/tools/maven"
Expand Down Expand Up @@ -147,6 +148,170 @@ func Test_MavenProject(t *testing.T) {
})
}

func Test_MavenProject_Package(t *testing.T) {
type args struct {
// service config to be packaged.
svc *ServiceConfig
// test setup parameter.
// file extension of java archives to create. empty means no archives are created.
archivesExt []string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
"Default",
args{
&ServiceConfig{
Project: &ProjectConfig{},
Name: "api",
RelativePath: "src/api",
Host: AppServiceTarget,
Language: ServiceLanguageJava,
EventDispatcher: ext.NewEventDispatcher[ServiceLifecycleEventArgs](),
},
[]string{".jar"},
},
false,
},
{
"SpecifyOutputDir",
args{&ServiceConfig{
Project: &ProjectConfig{},
Name: "api",
RelativePath: "src/api",
OutputPath: "mydir",
Host: AppServiceTarget,
Language: ServiceLanguageJava,
EventDispatcher: ext.NewEventDispatcher[ServiceLifecycleEventArgs](),
},
[]string{".war"},
},
false,
},
{
"SpecifyOutputFile",
args{&ServiceConfig{
Project: &ProjectConfig{},
Name: "api",
RelativePath: "src/api",
OutputPath: "mydir/ear.ear",
Host: AppServiceTarget,
Language: ServiceLanguageJava,
EventDispatcher: ext.NewEventDispatcher[ServiceLifecycleEventArgs](),
},
[]string{".ear"},
},
false,
},
{
"ErrNoArchive",
args{&ServiceConfig{
Project: &ProjectConfig{},
Name: "api",
RelativePath: "src/api",
Host: AppServiceTarget,
Language: ServiceLanguageJava,
EventDispatcher: ext.NewEventDispatcher[ServiceLifecycleEventArgs](),
},
[]string{},
},
true,
},
{
"ErrMultipleArchives",
args{&ServiceConfig{
Project: &ProjectConfig{},
Name: "api",
RelativePath: "src/api",
OutputPath: "mydir",
Host: AppServiceTarget,
Language: ServiceLanguageJava,
EventDispatcher: ext.NewEventDispatcher[ServiceLifecycleEventArgs](),
},
[]string{".jar", ".war", ".ear"},
},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
temp := t.TempDir()
// fill in the project path to avoid setting cwd
tt.args.svc.Project.Path = temp
svcDir := filepath.Join(temp, tt.args.svc.RelativePath)
require.NoError(t, os.MkdirAll(svcDir, osutil.PermissionDirectory))
err := os.WriteFile(filepath.Join(svcDir, getMvnwCmd()), nil, osutil.PermissionExecutableFile)
require.NoError(t, err)

var runArgs exec.RunArgs
mockContext := mocks.NewMockContext(context.Background())
mockContext.CommandRunner.
When(func(args exec.RunArgs, command string) bool {
return strings.Contains(command, fmt.Sprintf("%s package", getMvnwCmd()))
}).
RespondFn(func(args exec.RunArgs) (exec.RunResult, error) {
runArgs = args

packageSrcPath := filepath.Join(svcDir, tt.args.svc.OutputPath)
if strings.Contains(packageSrcPath, ".") { // an archive file path
err = os.MkdirAll(filepath.Dir(packageSrcPath), osutil.PermissionDirectory)
require.NoError(t, err)

err = os.WriteFile(packageSrcPath, []byte("test"), osutil.PermissionFile)
require.NoError(t, err)
} else { // a directory
if tt.args.svc.OutputPath == "" {
// default maven target directory
packageSrcPath = filepath.Join(packageSrcPath, "target")
}
err = os.MkdirAll(packageSrcPath, osutil.PermissionDirectory)
require.NoError(t, err)
for _, ext := range tt.args.archivesExt {
err = os.WriteFile(
// create a file that looks like jar.jar, ear.ear, war.war
filepath.Join(packageSrcPath, ext[1:]+ext),
[]byte("test"),
osutil.PermissionFile)
require.NoError(t, err)
}
}
return exec.NewRunResult(0, "", ""), nil
})

env := environment.Ephemeral()
mavenCli := maven.NewMavenCli(mockContext.CommandRunner)
javaCli := javac.NewCli(mockContext.CommandRunner)
mavenProject := NewMavenProject(env, mavenCli, javaCli)
err = mavenProject.Initialize(*mockContext.Context, tt.args.svc)
require.NoError(t, err)

packageTask := mavenProject.Package(
*mockContext.Context,
tt.args.svc,
&ServiceBuildResult{},
)
logProgress(packageTask)

result, err := packageTask.Await()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, result)
require.NotEmpty(t, result.PackagePath)
require.Contains(t, runArgs.Cmd, getMvnwCmd())
require.Equal(t,
[]string{"package", "-DskipTests"},
runArgs.Args,
)
}
})
}
}

func getMvnwCmd() string {
if runtime.GOOS == "windows" {
return "mvnw.cmd"
Expand Down
4 changes: 2 additions & 2 deletions schemas/v1.0/azure.yaml.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
"properties": {
"dist": {
"type": "string",
"description": "The CLI will use the JAR file in this directory to create the deployment artifact (ZIP file). If omitted, the CLI will detect the output directory based on the build system in-use."
"description": "Optional. The path to the directory containing a single Java archive file (.jar/.ear/.war), or the path to the specific Java archive file to be included in the deployment artifact. If omitted, the CLI will detect the output directory based on the build system in-use. For maven, the directory 'target' is assumed."
weikanglim marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand All @@ -220,7 +220,7 @@
"properties": {
"dist": {
"type": "string",
"description": "The CLI will use files under this path to create the deployment artifact (ZIP file). If omitted, all files under service project directory will be included."
"description": "Optional. The CLI will use files under this path to create the deployment artifact (ZIP file). If omitted, all files under service project directory will be included."
}
}
}
Expand Down