Skip to content

Commit edba5eb

Browse files
committed
adding custom subcommand
1 parent 3055b79 commit edba5eb

File tree

8 files changed

+309
-11
lines changed

8 files changed

+309
-11
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using Azure.Functions.Cli.Interfaces;
5+
using Fclp;
6+
7+
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
8+
{
9+
[Action(Name = "pack custom", ParentCommandName = "pack", ShowInHelp = true, HelpText = "Arguments specific to custom worker runtime apps when running func pack")]
10+
internal class CustomPackSubcommandAction : BaseAction
11+
{
12+
public override ICommandLineParserResult ParseArgs(string[] args)
13+
{
14+
return base.ParseArgs(args);
15+
}
16+
17+
public async Task RunAsync(PackOptions packOptions)
18+
{
19+
// No build or specific action required before zipping; just perform zip
20+
await PackHelpers.DefaultZip(packOptions, TelemetryCommandEvents);
21+
}
22+
23+
public override Task RunAsync()
24+
{
25+
// Keep this since this subcommand is not meant to be run directly.
26+
return Task.CompletedTask;
27+
}
28+
}
29+
}

src/Cli/func/Actions/LocalActions/PackAction/DotnetPackSubcommandAction.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,7 @@ public async Task RunAsync(PackOptions packOptions)
7373
var outputPath = PackHelpers.ResolveOutputPath(functionAppRoot, packOptions.OutputPath);
7474
PackHelpers.CleanupExistingPackage(outputPath);
7575

76-
// Install extensions if not in no-build mode
77-
if (!packOptions.NoBuild)
78-
{
79-
var installExtensionAction = new InstallExtensionAction(_secretsManager, false);
80-
await installExtensionAction.RunAsync();
81-
}
82-
83-
await PackHelpers.CreatePackage(packingRoot, outputPath, packOptions.NoBuild, TelemetryCommandEvents);
76+
await PackHelpers.CreatePackage(packingRoot, outputPath, packOptions.NoBuild, TelemetryCommandEvents, packOptions.PreserveExecutables);
8477
}
8578

8679
public override Task RunAsync()
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Runtime.InteropServices;
5+
using Azure.Functions.Cli.Common;
6+
using Azure.Functions.Cli.Helpers;
7+
using Azure.Functions.Cli.Interfaces;
8+
using Colors.Net;
9+
using Fclp;
10+
using static Azure.Functions.Cli.Common.OutputTheme;
11+
12+
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
13+
{
14+
[Action(Name = "pack node", ParentCommandName = "pack", ShowInHelp = true, HelpText = "Arguments specific to Node.js apps when running func pack")]
15+
internal class NodePackSubcommandAction : BaseAction
16+
{
17+
private readonly ISecretsManager _secretsManager;
18+
19+
public NodePackSubcommandAction(ISecretsManager secretsManager)
20+
{
21+
_secretsManager = secretsManager;
22+
}
23+
24+
public bool SkipInstall { get; set; }
25+
26+
public override ICommandLineParserResult ParseArgs(string[] args)
27+
{
28+
Parser
29+
.Setup<bool>("skip-install")
30+
.WithDescription("Skips running 'npm install' when packing the function app.")
31+
.Callback(o => SkipInstall = o);
32+
33+
return base.ParseArgs(args);
34+
}
35+
36+
public async Task RunAsync(PackOptions packOptions, string[] args)
37+
{
38+
// Parse Node.js-specific arguments
39+
ParseArgs(args);
40+
41+
var functionAppRoot = PackHelpers.ResolveFunctionAppRoot(packOptions.FolderPath);
42+
43+
if (!Directory.Exists(functionAppRoot))
44+
{
45+
throw new CliException($"Directory not found to pack: {functionAppRoot}");
46+
}
47+
48+
// Validate package.json exists
49+
ValidateNodeJsProject(functionAppRoot);
50+
51+
// Run Node.js build process if not skipping
52+
if (!packOptions.NoBuild)
53+
{
54+
await RunNodeJsBuildProcess(functionAppRoot);
55+
}
56+
57+
var outputPath = PackHelpers.ResolveOutputPath(functionAppRoot, packOptions.OutputPath);
58+
PackHelpers.CleanupExistingPackage(outputPath);
59+
60+
await PackHelpers.CreatePackage(functionAppRoot, outputPath, packOptions.NoBuild, TelemetryCommandEvents, packOptions.PreserveExecutables);
61+
}
62+
63+
public override Task RunAsync()
64+
{
65+
// This method is called when someone tries to run "func pack node" directly
66+
return Task.CompletedTask;
67+
}
68+
69+
private void ValidateNodeJsProject(string functionAppRoot)
70+
{
71+
var packageJsonPath = Path.Combine(functionAppRoot, "package.json");
72+
if (!FileSystemHelpers.FileExists(packageJsonPath))
73+
{
74+
throw new CliException($"package.json not found in {functionAppRoot}. This is required for Node.js function apps.");
75+
}
76+
77+
if (StaticSettings.IsDebug)
78+
{
79+
ColoredConsole.WriteLine(VerboseColor($"Found package.json at {packageJsonPath}"));
80+
}
81+
}
82+
83+
private async Task RunNodeJsBuildProcess(string functionAppRoot)
84+
{
85+
// Ensure npm is available
86+
EnsureNpmExists();
87+
88+
// Change to the function app directory for npm operations
89+
var previousDirectory = Environment.CurrentDirectory;
90+
try
91+
{
92+
Environment.CurrentDirectory = functionAppRoot;
93+
94+
// Run npm install if not skipped
95+
if (!SkipInstall)
96+
{
97+
await NpmHelper.Install();
98+
Console.WriteLine();
99+
}
100+
101+
// Check if build script exists and run it
102+
await RunNpmBuildIfExists();
103+
}
104+
finally
105+
{
106+
// Restore the previous directory
107+
Environment.CurrentDirectory = previousDirectory;
108+
}
109+
}
110+
111+
private async Task RunNpmBuildIfExists()
112+
{
113+
try
114+
{
115+
// Check if package.json has a build script
116+
var packageJsonPath = Path.Combine(Environment.CurrentDirectory, "package.json");
117+
if (FileSystemHelpers.FileExists(packageJsonPath))
118+
{
119+
var packageJsonContent = await FileSystemHelpers.ReadAllTextFromFileAsync(packageJsonPath);
120+
121+
// Simple check if build script exists
122+
if (packageJsonContent.Contains("\"build\"") && packageJsonContent.Contains("scripts"))
123+
{
124+
ColoredConsole.WriteLine("Running npm run build...");
125+
await NpmHelper.RunNpmCommand("run build", ignoreError: false);
126+
}
127+
else
128+
{
129+
if (StaticSettings.IsDebug)
130+
{
131+
ColoredConsole.WriteLine(VerboseColor("No build script found in package.json, skipping npm run build."));
132+
}
133+
}
134+
}
135+
}
136+
catch (Exception ex)
137+
{
138+
throw new CliException($"npm run build failed: {ex.Message}");
139+
}
140+
}
141+
142+
private static void EnsureNpmExists()
143+
{
144+
if (!CommandChecker.CommandExists("npm"))
145+
{
146+
throw new CliException("npm is required for Node.js function apps. Please install Node.js and npm from https://nodejs.org/");
147+
}
148+
149+
if (StaticSettings.IsDebug)
150+
{
151+
ColoredConsole.WriteLine(VerboseColor("npm command found and available."));
152+
}
153+
}
154+
}
155+
}

src/Cli/func/Actions/LocalActions/PackAction/PackAction.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Azure.Functions.Cli.Interfaces;
77
using Colors.Net;
88
using Fclp;
9+
using Microsoft.Azure.AppService.Proxy.Common.Constants;
910
using Microsoft.Azure.WebJobs.Script;
1011

1112
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
@@ -28,6 +29,8 @@ public PackAction(ISecretsManager secretsManager)
2829

2930
public string[] PreserveExecutables { get; set; } = Array.Empty<string>();
3031

32+
private string[] Args { get; set; }
33+
3134
public override ICommandLineParserResult ParseArgs(string[] args)
3235
{
3336
Parser
@@ -51,6 +54,8 @@ public override ICommandLineParserResult ParseArgs(string[] args)
5154
FolderPath = args.First();
5255
}
5356

57+
Args = args;
58+
5459
return base.ParseArgs(args);
5560
}
5661

@@ -76,6 +81,10 @@ private async Task RunRuntimeSpecificPackAsync(WorkerRuntime runtime, PackOption
7681
await (runtime switch
7782
{
7883
WorkerRuntime.Dotnet or WorkerRuntime.DotnetIsolated => new DotnetPackSubcommandAction(_secretsManager).RunAsync(packOptions),
84+
WorkerRuntime.Python => new PythonPackSubcommandAction(_secretsManager).RunAsync(packOptions, Args),
85+
WorkerRuntime.Node => new NodePackSubcommandAction(_secretsManager).RunAsync(packOptions, Args),
86+
WorkerRuntime.Powershell => new PowershellPackSubcommandAction().RunAsync(packOptions),
87+
WorkerRuntime.Custom => new CustomPackSubcommandAction().RunAsync(packOptions),
7988
_ => throw new CliException($"Unsupported runtime: {runtime}")
8089
});
8190
}

src/Cli/func/Actions/LocalActions/PackAction/PackHelpers.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public static void CleanupExistingPackage(string outputPath)
6262
}
6363
}
6464

65-
public static async Task CreatePackage(string packingRoot, string outputPath, bool noBuild, IDictionary<string, string> telemetryCommandEvents, bool buildNativeDeps = false)
65+
public static async Task CreatePackage(string packingRoot, string outputPath, bool noBuild, IDictionary<string, string> telemetryCommandEvents, string[] executables, bool buildNativeDeps = false)
6666
{
6767
bool useGoZip = EnvironmentHelper.GetEnvironmentVariableAsBool(Constants.UseGoZip);
6868
TelemetryHelpers.AddCommandEventToDictionary(telemetryCommandEvents, "UseGoZip", useGoZip.ToString());
@@ -72,5 +72,20 @@ public static async Task CreatePackage(string packingRoot, string outputPath, bo
7272
ColoredConsole.WriteLine($"Creating a new package {outputPath}");
7373
await FileSystemHelpers.WriteToFile(outputPath, stream);
7474
}
75+
76+
public static async Task DefaultZip(PackOptions packOptions, IDictionary<string, string> telemetryCommandEvents)
77+
{
78+
var functionAppRoot = ResolveFunctionAppRoot(packOptions.FolderPath);
79+
80+
if (!Directory.Exists(functionAppRoot))
81+
{
82+
throw new CliException($"Directory not found to pack: {functionAppRoot}");
83+
}
84+
85+
var outputPath = ResolveOutputPath(functionAppRoot, packOptions.OutputPath);
86+
CleanupExistingPackage(outputPath);
87+
88+
await CreatePackage(functionAppRoot, outputPath, packOptions.NoBuild, telemetryCommandEvents, packOptions.PreserveExecutables);
89+
}
7590
}
7691
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using Azure.Functions.Cli.Interfaces;
5+
using Fclp;
6+
7+
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
8+
{
9+
[Action(Name = "pack powershell", ParentCommandName = "pack", ShowInHelp = true, HelpText = "Arguments specific to PowerShell apps when running func pack")]
10+
internal class PowershellPackSubcommandAction : BaseAction
11+
{
12+
public override ICommandLineParserResult ParseArgs(string[] args)
13+
{
14+
return base.ParseArgs(args);
15+
}
16+
17+
public async Task RunAsync(PackOptions packOptions)
18+
{
19+
// No build or specific action required before zipping; just perform zip
20+
await PackHelpers.DefaultZip(packOptions, TelemetryCommandEvents);
21+
}
22+
23+
public override Task RunAsync()
24+
{
25+
// Keep this since this subcommand is not meant to be run directly.
26+
return Task.CompletedTask;
27+
}
28+
}
29+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Runtime.InteropServices;
5+
using Azure.Functions.Cli.Common;
6+
using Azure.Functions.Cli.Interfaces;
7+
using Colors.Net;
8+
using Fclp;
9+
using static Azure.Functions.Cli.Common.OutputTheme;
10+
11+
namespace Azure.Functions.Cli.Actions.LocalActions.PackAction
12+
{
13+
[Action(Name = "pack python", ParentCommandName = "pack", ShowInHelp = true, HelpText = "Arguments specific to Python apps when running func pack")]
14+
internal class PythonPackSubcommandAction : BaseAction
15+
{
16+
private readonly ISecretsManager _secretsManager;
17+
18+
public PythonPackSubcommandAction(ISecretsManager secretsManager)
19+
{
20+
_secretsManager = secretsManager;
21+
}
22+
23+
public bool BuildNativeDeps { get; set; }
24+
25+
public override ICommandLineParserResult ParseArgs(string[] args)
26+
{
27+
Parser
28+
.Setup<bool>("build-native-deps")
29+
.WithDescription("Builds function app locally using an image that was previously hosted." +
30+
" When enabled, core tools launches a docker container with the build env images, builds the function app inside the container," +
31+
" and creates a ZIP file with all dependencies restored in .python_packages")
32+
.Callback(o => BuildNativeDeps = o);
33+
34+
return base.ParseArgs(args);
35+
}
36+
37+
public async Task RunAsync(PackOptions packOptions, string[] args)
38+
{
39+
ParseArgs(args);
40+
41+
var functionAppRoot = PackHelpers.ResolveFunctionAppRoot(packOptions.FolderPath);
42+
43+
if (!Directory.Exists(functionAppRoot))
44+
{
45+
throw new CliException($"Directory not found to pack: {functionAppRoot}");
46+
}
47+
48+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
49+
{
50+
ColoredConsole.WriteLine(WarningColor("Python function apps is supported only on Linux. Please use the --build-native-deps flag" +
51+
" when building on windows to ensure dependencies are properly restored."));
52+
}
53+
54+
var outputPath = PackHelpers.ResolveOutputPath(functionAppRoot, packOptions.OutputPath);
55+
PackHelpers.CleanupExistingPackage(outputPath);
56+
57+
await PackHelpers.CreatePackage(functionAppRoot, outputPath, packOptions.NoBuild, TelemetryCommandEvents, packOptions.PreserveExecutables, BuildNativeDeps);
58+
}
59+
60+
public override Task RunAsync()
61+
{
62+
// Keep this since this subcommand is not meant to be run directly.
63+
return Task.CompletedTask;
64+
}
65+
}
66+
}

src/Cli/func/Helpers/ZipHelper.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

44
using System.IO.Compression;
@@ -12,7 +12,7 @@ namespace Azure.Functions.Cli.Helpers
1212
{
1313
public static class ZipHelper
1414
{
15-
public static async Task<Stream> GetAppZipFile(string functionAppRoot, bool buildNativeDeps, BuildOption buildOption, bool noBuild, GitIgnoreParser ignoreParser = null, string additionalPackages = null)
15+
public static async Task<Stream> GetAppZipFile(string functionAppRoot, bool buildNativeDeps, BuildOption buildOption, bool noBuild, GitIgnoreParser ignoreParser = null, string additionalPackages = null, string[] preserveExecutables = null)
1616
{
1717
var gitIgnorePath = Path.Combine(functionAppRoot, Constants.FuncIgnoreFile);
1818
if (ignoreParser == null && FileSystemHelpers.FileExists(gitIgnorePath))
@@ -48,6 +48,8 @@ public static async Task<Stream> GetAppZipFile(string functionAppRoot, bool buil
4848
IEnumerable<string> executables = !string.IsNullOrEmpty(customHandler)
4949
? new[] { customHandler }
5050
: Enumerable.Empty<string>();
51+
52+
executables = executables.Concat(preserveExecutables ?? Enumerable.Empty<string>());
5153
return await CreateZip(FileSystemHelpers.GetLocalFiles(functionAppRoot, ignoreParser, false), functionAppRoot, executables);
5254
}
5355
}

0 commit comments

Comments
 (0)