Skip to content

Commit 3d7d831

Browse files
authored
Refactor func pack (#4600)
1 parent 9f072f1 commit 3d7d831

40 files changed

+1840
-174
lines changed

eng/ci/templates/jobs/test-e2e-linux.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
python_linux_x64:
2323
languageWorker: 'Python'
2424
runtime: 'linux-x64'
25+
custom_linux_x64:
26+
languageWorker: 'Custom'
27+
runtime: 'linux-x64'
2528

2629
steps:
2730
- pwsh: ./eng/scripts/start-emulators.ps1

eng/ci/templates/jobs/test-e2e-osx.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
python_osx_x64:
2323
languageWorker: 'Python'
2424
runtime: 'osx-x64'
25+
custom_osx_x64:
26+
languageWorker: 'Custom'
27+
runtime: 'osx-x64'
2528

2629
steps:
2730
- pwsh: ./eng/scripts/start-emulators.ps1

eng/ci/templates/jobs/test-e2e-windows.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
python_win_x64:
2323
languageWorker: 'Python'
2424
runtime: 'win-x64'
25+
custom_win_x64:
26+
languageWorker: 'Custom'
27+
runtime: 'win-x64'
2528

2629
steps:
2730
- pwsh: ./eng/scripts/start-emulators.ps1 -NoWait

eng/ci/templates/steps/run-e2e-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ steps:
3030
Write-Host "##vso[task.setvariable variable=DURABLE_FUNCTION_PATH]$(Build.SourcesDirectory)/test/Azure.Functions.Cli.Tests/Resources/DurableTestFolder"
3131
Write-Host "##vso[task.setvariable variable=INPROC_RUN_SETTINGS]$(Build.SourcesDirectory)/test/Cli/Func.E2ETests/.runsettings/start_tests/ci_pipeline/dotnet_inproc.runsettings"
3232
Write-Host "##vso[task.setvariable variable=TEST_PROJECT_PATH]$(Build.SourcesDirectory)/test/TestFunctionApps"
33+
Write-Host "##vso[task.setvariable variable=FUNCTIONS_PYTHON_DOCKER_IMAGE]mcr.microsoft.com/azure-functions/python:4-python3.11-buildenv"
3334
displayName: 'Set environment variables for E2E tests'
3435

3536

release_notes.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Azure Functions CLI 4.2.3
1+
# Azure Functions CLI 4.3.0
2+
23

34
#### Host Version
45

56
- Host Version: 4.1041.200
67
- In-Proc Host Version: 4.41.100 (4.841.100, 4.641.100)
78

89
#### Changes
9-
10-
- Updated assemblies to sign: AppService, Functions, WebJobs; consolidated OpenTelemetry binaries; removed Python exec patterns (t32, t64, w32, w64) (#4621)
10+
- Add `func pack` basic functionality (#4600)

src/Cli/func/ActionAttribute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ internal sealed class ActionAttribute : Attribute
1515
public string HelpText { get; set; } = "placeholder";
1616

1717
public bool ShowInHelp { get; set; } = true;
18+
19+
public string ParentCommandName { get; set; } = string.Empty;
1820
}
1921
}

src/Cli/func/ActionType.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ internal class ActionType
1212
public IEnumerable<Context> SubContexts { get; set; }
1313

1414
public IEnumerable<string> Names { get; set; }
15+
16+
public IEnumerable<string> ParentCommandName { get; set; }
1517
}
1618
}

src/Cli/func/Actions/HelpAction.cs

Lines changed: 82 additions & 10 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.ComponentModel;
@@ -18,6 +18,9 @@ namespace Azure.Functions.Cli.Actions
1818
{
1919
internal class HelpAction : BaseAction
2020
{
21+
// Standardized indentation
22+
private const int IndentSize = 4;
23+
2124
private readonly string _context;
2225
private readonly string _subContext;
2326
private readonly IAction _action;
@@ -42,7 +45,8 @@ public HelpAction(IEnumerable<TypeAttributePair> actions, Func<Type, IAction> cr
4245
Type = type,
4346
Contexts = attributes.Select(a => a.Context),
4447
SubContexts = attributes.Select(a => a.SubContext),
45-
Names = attributes.Select(a => a.Name)
48+
Names = attributes.Select(a => a.Name),
49+
ParentCommandName = attributes.Select(a => a.ParentCommandName)
4650
};
4751
});
4852
}
@@ -54,6 +58,8 @@ public HelpAction(IEnumerable<TypeAttributePair> actions, Func<Type, IAction> cr
5458
_parseResult = parseResult;
5559
}
5660

61+
private static string Indent(int levels = 1) => new string(' ', IndentSize * (levels < 0 ? 0 : levels));
62+
5763
public override async Task RunAsync()
5864
{
5965
var latestVersionMessageTask = VersionHelper.IsRunningAnOlderVersion();
@@ -187,7 +193,8 @@ private void DisplayGeneralHelp()
187193
.WriteLine("Usage: func [context] <action> [-/--options]")
188194
.WriteLine();
189195
DisplayContextsHelp(contexts);
190-
var actions = _actionTypes.Where(a => a.Contexts.Contains(Context.None));
196+
var actions = _actionTypes
197+
.Where(a => a.Contexts.Contains(Context.None));
191198
DisplayActionsHelp(actions);
192199
}
193200

@@ -211,15 +218,80 @@ private void DisplayActionsHelp(IEnumerable<ActionType> actions)
211218
if (actions.Any())
212219
{
213220
ColoredConsole.WriteLine(TitleColor("Actions: "));
221+
222+
// Group actions by parent command
223+
var parentCommands = actions
224+
.Where(a => a.ParentCommandName.All(p => string.IsNullOrEmpty(p))) // Actions with no parent
225+
.ToList();
226+
227+
var subCommands = actions
228+
.Where(a => a.ParentCommandName.Any(p => !string.IsNullOrEmpty(p))) // Actions with a parent
229+
.ToList();
230+
214231
var longestName = actions.Select(a => a.Names).SelectMany(n => n).Max(n => n.Length);
215232
longestName += 2; // for coloring chars
216-
foreach (var action in actions)
233+
234+
// Display parent commands first
235+
foreach (var parentAction in parentCommands)
217236
{
218-
ColoredConsole.WriteLine(GetActionHelp(action, longestName));
219-
DisplaySwitches(action);
237+
// Display parent command
238+
ColoredConsole.WriteLine(GetActionHelp(parentAction, longestName));
239+
DisplaySwitches(parentAction);
240+
241+
// Find and display child commands for this parent
242+
var parentName = parentAction.Names.First();
243+
var childCommands = subCommands
244+
.Where(s => s.ParentCommandName.Any(p => p.Equals(parentName, StringComparison.OrdinalIgnoreCase)))
245+
.ToList();
246+
247+
if (childCommands.Any())
248+
{
249+
ColoredConsole.WriteLine(); // Add spacing before subcommands
250+
251+
foreach (var childCommand in childCommands)
252+
{
253+
DisplaySubCommandHelp(childCommand);
254+
}
255+
}
256+
257+
ColoredConsole.WriteLine();
220258
}
259+
}
260+
}
221261

222-
ColoredConsole.WriteLine();
262+
private void DisplaySubCommandHelp(ActionType subCommand)
263+
{
264+
// Ensure subCommand is valid
265+
if (subCommand is null)
266+
{
267+
return;
268+
}
269+
270+
// Extract the runtime name from the full command name
271+
// E.g., "pack dotnet" -> "Dotnet"
272+
var fullCommandName = subCommand.Names?.FirstOrDefault();
273+
274+
string runtimeName = null;
275+
if (!string.IsNullOrWhiteSpace(fullCommandName))
276+
{
277+
var parts = fullCommandName.Split(' ', StringSplitOptions.RemoveEmptyEntries);
278+
runtimeName = parts.Length > 1 && !string.IsNullOrEmpty(parts[1])
279+
? char.ToUpper(parts[1][0]) + parts[1].Substring(1).ToLower()
280+
: fullCommandName;
281+
}
282+
283+
// Fall back to a safe default if we couldn't determine a runtime name
284+
runtimeName ??= subCommand.Type?.Name ?? "subcommand";
285+
286+
var description = subCommand.Type?.GetCustomAttributes<ActionAttribute>()?.FirstOrDefault()?.HelpText;
287+
288+
// Display indented subcommand header with standardized indentation
289+
ColoredConsole.WriteLine($"{Indent(1)}{runtimeName.DarkCyan()}{Indent(2)}{description}");
290+
291+
// Display subcommand switches with extra indentation
292+
if (subCommand.Type != null)
293+
{
294+
DisplaySwitches(subCommand);
223295
}
224296
}
225297

@@ -261,7 +333,7 @@ private void DisplayPositionalArguments(IEnumerable<CliArgument> arguments)
261333
longestName += 4; // 4 for coloring and <> characters
262334
foreach (var argument in arguments)
263335
{
264-
var helpLine = string.Format($" {{0, {-longestName}}} {{1}}", $"<{argument.Name}>".DarkGray(), argument.Description);
336+
var helpLine = string.Format($"{Indent(1)}{{0, {-longestName}}} {{1}}", $"<{argument.Name}>".DarkGray(), argument.Description);
265337
if (helpLine.Length < SafeConsole.BufferWidth)
266338
{
267339
ColoredConsole.WriteLine(helpLine);
@@ -277,7 +349,7 @@ private void DisplayPositionalArguments(IEnumerable<CliArgument> arguments)
277349
}
278350
}
279351

280-
private static void DisplayOptions(IEnumerable<ICommandLineOption> options)
352+
private static void DisplayOptions(IEnumerable<ICommandLineOption> options, bool addExtraIndent = false)
281353
{
282354
var longestName = options.Max(o =>
283355
{
@@ -311,7 +383,7 @@ private static void DisplayOptions(IEnumerable<ICommandLineOption> options)
311383
stringBuilder.Append($" [-{option.ShortName}]");
312384
}
313385

314-
var helpSwitch = string.Format($" {{0, {-longestName}}} ", stringBuilder.ToString().DarkGray());
386+
var helpSwitch = string.Format($"{(addExtraIndent ? Indent(2) : Indent(1))}{{0, {-longestName}}} ", stringBuilder.ToString().DarkGray());
315387
var helpSwitchLength = helpSwitch.Length - 2; // helpSwitch contains 2 formatting characters.
316388
var helpText = option.Description;
317389
if (string.IsNullOrWhiteSpace(helpText))

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

Lines changed: 0 additions & 123 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 = false, HelpText = "Arguments specific to custom worker runtime apps when running func pack")]
10+
internal class CustomPackSubcommandAction : PackSubcommandAction
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+
await ExecuteAsync(packOptions);
20+
}
21+
22+
protected override Task<string> GetPackingRootAsync(string functionAppRoot, PackOptions options)
23+
{
24+
// Custom worker packs from the function app root without extra steps
25+
return Task.FromResult(functionAppRoot);
26+
}
27+
28+
public override Task RunAsync()
29+
{
30+
// Keep this since this subcommand is not meant to be run directly.
31+
return Task.CompletedTask;
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)