How to generate Resources (Catalog) class with all addresses?

When we develop for android the IDE dynamically generates “R” class which contains constants for all assets. Needless to say, it’s very convenient.
But why is there no such thing in Unity?
How can I extend the build pipeline to create the “R” class along with asset bundles?
Strange, but I didn’t find anything.

namespace UnityEditor.AddressableAssets {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using UnityEditor;
    using UnityEditor.AddressableAssets.Settings;
    using UnityEngine;

    public static class AssetsSourceGenerator {

        // Generate
        public static void Generate(string path, string @namespace, string @class, AddressableAssetSettings settings) {
            var builder = new StringBuilder();
            var entries = settings.GetEntries().ToTreeList().Items.ToArray();
            builder.AppendCompilationUnit( @namespace, @class, entries );
            WriteText( path, builder.ToString() );
        }
        private static void WriteText(string path, string text) {
            if (!File.Exists( path ) || File.ReadAllText( path ) != text) {
                File.WriteAllText( path, text );
                AssetDatabase.ImportAsset( path, ImportAssetOptions.Default );
            }
        }

        // Append
        private static void AppendCompilationUnit(this StringBuilder builder, string @namespace, string @class, KeyValueTreeList<AddressableAssetEntry>.Item[] items) {
            builder.AppendLine( $"namespace {@namespace} {{" );
            {
                builder.AppendClass( 1, @class, items );
            }
            builder.AppendLine( "}" );
        }
        private static void AppendClass(this StringBuilder builder, int indent, string @class, KeyValueTreeList<AddressableAssetEntry>.Item[] items) {
            builder.AppendIndent( indent ).AppendLine( $"public static class {@class} {{" );
            foreach (var item in items.Sort_()) {
                builder.AppendValueOrScope( indent + 1, item );
            }
            builder.AppendIndent( indent ).AppendLine( "}" );
        }
        private static void AppendValueOrScope(this StringBuilder builder, int indent, KeyValueTreeList<AddressableAssetEntry>.Item item) {
            if (item is KeyValueTreeList<AddressableAssetEntry>.ValueItem value) {
                var identifier = value.Key;
                var address = value.Value.address;
                if (value.Value.IsAsset()) {
                    builder.AppendIndent( indent ).AppendLine( $"public const string @{identifier} = \"{address}\";" );
                } else {
                    throw Exceptions.Internal.NotSupported( $"Entry {value.Value} is not supported" );
                }
            } else
            if (item is KeyValueTreeList<AddressableAssetEntry>.ScopeItem scope) {
                var identifier = scope.Key;
                var items = scope.Items;
                builder.AppendIndent( indent ).AppendLine( $"public static class @{identifier} {{" );
                foreach (var i in items.Sort_()) {
                    builder.AppendValueOrScope( indent + 1, i );
                }
                builder.AppendIndent( indent ).AppendLine( "}" );
            }
        }

        // Helpers
        private static IEnumerable<KeyValueTreeList<AddressableAssetEntry>.Item> Sort_(this IEnumerable<KeyValueTreeList<AddressableAssetEntry>.Item> items) {
            return items
                .OrderByDescending( i => i.Key.Equals( "EditorSceneList" ) )
                .ThenByDescending( i => i.Key.Equals( "Resources" ) )

                .ThenByDescending( i => i.Key.Equals( "UnityEngine" ) )
                .ThenByDescending( i => i.Key.Equals( "UnityEditor" ) )

                .ThenByDescending( i => i.Key.Equals( "Scenes" ) )
                .ThenByDescending( i => i.Key.Equals( "Launcher" ) )
                .ThenByDescending( i => i.Key.Equals( "LauncherScene" ) )
                .ThenByDescending( i => i.Key.Equals( "Startup" ) )
                .ThenByDescending( i => i.Key.Equals( "StartupScene" ) )
                .ThenByDescending( i => i.Key.Equals( "Program" ) )
                .ThenByDescending( i => i.Key.Equals( "ProgramScene" ) )
                .ThenByDescending( i => i.Key.Equals( "MainScene" ) )
                .ThenByDescending( i => i.Key.Equals( "GameScene" ) )

                .ThenByDescending( i => i.Key.Equals( "Program" ) )
                .ThenByDescending( i => i.Key.Equals( "Presentation" ) )
                .ThenByDescending( i => i.Key.Equals( "UI" ) )
                .ThenByDescending( i => i.Key.Equals( "App" ) )
                .ThenByDescending( i => i.Key.Equals( "Game" ) )
                .ThenByDescending( i => i.Key.Equals( "Core" ) )

                .ThenByDescending( i => i.Key.Equals( "MainScreen" ) )
                .ThenByDescending( i => i.Key.Equals( "GameScreen" ) )

                .ThenByDescending( i => i.Key.Equals( "Objects" ) )
                .ThenByDescending( i => i.Key.Equals( "Subjects" ) )
                .ThenByDescending( i => i.Key.Equals( "World" ) )
                .ThenByDescending( i => i.Key.Equals( "Levels" ) )

                .ThenBy( i => i.Key );
        }

    }
}
namespace UnityEditor.AddressableAssets {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using UnityEditor.AddressableAssets.Settings;
    using UnityEngine;

    internal static class AssetsSourceGeneratorHelper {

        // GetEntries
        public static List<AddressableAssetEntry> GetEntries(this AddressableAssetSettings settings) {
            var entries = new List<AddressableAssetEntry>();
            settings.GetAllAssets( entries, true );
            return entries;
        }

        // ToTreeList
        public static KeyValueTreeList<AddressableAssetEntry> ToTreeList(this IList<AddressableAssetEntry> entries) {
            var treeList = new KeyValueTreeList<AddressableAssetEntry>();
            foreach (var entry in entries) {
                var (scope, identifier) = entry.GetPath();
                treeList.AddValue( scope, identifier, entry );
            }
            return treeList;
        }

        // IsAsset
        public static bool IsAsset(this AddressableAssetEntry entry) {
            return !entry.IsFolder;
        }
        public static bool IsMainAsset(this AddressableAssetEntry entry) {
            if (!entry.IsFolder) {
                return entry.ParentEntry == null || entry.ParentEntry.IsFolder;
            }
            return false;
        }
        public static bool IsSubAsset(this AddressableAssetEntry entry) {
            if (!entry.IsFolder) {
                return entry.ParentEntry != null && !entry.ParentEntry.IsFolder;
            }
            return false;
        }
        public static bool IsFolder(this AddressableAssetEntry entry) {
            return entry.IsFolder;
        }

        // Helpers
        private static (string[] Scope, string Identifier) GetPath(this AddressableAssetEntry entry) {
            if (entry.IsAsset()) {
                if (entry.IsMainAsset()) {
                    var scope = entry.GetScope();
                    var identifier = entry.GetIdentifier();
                    return (scope, identifier);
                } else {
                    var scope = entry.ParentEntry.GetScope();
                    var identifier = entry.ParentEntry.GetIdentifier();
                    return (scope.Append( identifier + "_" ).ToArray(), entry.TargetAsset.name.Escape());
                }
            } else {
                throw Exceptions.Internal.NotSupported( $"Entry {entry} is not supported" );
            }
        }
        private static string[] GetScope(this AddressableAssetEntry entry) {
            var scope = Path.GetDirectoryName( entry.address );
            return scope.Split( '/', '\\', '.' ).Select( Escape ).ToArray();
        }
        private static string GetIdentifier(this AddressableAssetEntry entry) {
            var identifier = Path.GetFileNameWithoutExtension( entry.address );
            if (identifier.Contains( " #" )) identifier = identifier.Substring( 0, identifier.IndexOf( " #" ) );
            if (identifier.Contains( " @" )) identifier = identifier.Substring( 0, identifier.IndexOf( " @" ) );
            return identifier.Escape();
        }
        private static string Escape(this string value) {
            var chars = value.ToCharArray();
            for (var i = 0; i < chars.Length; i++) {
                if (!char.IsLetterOrDigit( chars[ i ] )) chars[ i ] = '_';
            }
            return new string( chars );
        }

    }
}
namespace System.Collections.Generic {
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    public partial class KeyValueTreeList<T> {
        internal interface IScope {
            List<KeyValueTreeList<T>.Item> Items { get; }
        }
        public abstract class Item {
            public string Key { get; }
            public Item(string key) {
                Key = key;
            }
        }
        public class ValueItem : Item {
            public T Value { get; }
            public ValueItem(string key, T value) : base( key ) {
                Value = value;
            }
        }
        public class ScopeItem : Item, IScope {
            public List<KeyValueTreeList<T>.Item> Items { get; } = new List<KeyValueTreeList<T>.Item>( 0 );
            public ScopeItem(string key) : base( key ) {
            }
        }
    }
    public partial class KeyValueTreeList<T> : KeyValueTreeList<T>.IScope {

        public List<KeyValueTreeList<T>.Item> Items { get; } = new List<Item>();

        // Constructor
        public KeyValueTreeList() {
        }

        // GetValue
        public T? GetValue(string[] keys, string key) {
            var scope = GetScope( this, keys );
            if (scope != null) {
                return scope.Items.OfType<ValueItem>().Where( i => i.Key == key ).Select( i => i.Value ).FirstOrDefault();
            }
            return default;
        }
        public T[]? GetValues(string[] keys, string key) {
            var scope = GetScope( this, keys );
            if (scope != null) {
                return scope.Items.OfType<ValueItem>().Where( i => i.Key == key ).Select( i => i.Value ).ToArray();
            }
            return default;
        }
        // AddValue
        public void AddValue(string[] keys, string key, T value) {
            var scope = GetOrAddScope( this, keys );
            scope.Items.Add( new ValueItem( key, value ) );
        }
        public void AddValues(string[] keys, string key, params T[] values) {
            var scope = GetOrAddScope( this, keys );
            foreach (var value in values) {
                scope.Items.Add( new ValueItem( key, value ) );
            }
        }
        // RemoveAll
        public int RemoveAll(string[] keys, string key) {
            var scope = GetScope( this, keys );
            if (scope != null) {
                return scope.Items.RemoveAll( i => i.Key == key );
            }
            return 0;
        }

        // Utils
        public override string ToString() {
            var builder = new StringBuilder();
            GetString( builder, this );
            return builder.ToString();
        }

        // Helpers/GetScope
        private static IScope? GetScope(IScope scope, IEnumerable<string> keys) {
            var subScope = scope;
            foreach (var key in keys) {
                subScope = GetScope( subScope, key );
                if (subScope == null) break;
            }
            return subScope;
        }
        private static IScope GetOrAddScope(IScope scope, IEnumerable<string> keys) {
            var subScope = scope;
            foreach (var key in keys) {
                subScope = GetOrAddScope( subScope, key );
            }
            return subScope;
        }

        // Helpers/GetScope
        private static ScopeItem? GetScope(IScope scope, string key) {
            var subScope = scope.Items.OfType<ScopeItem>().Where( i => i.Key == key ).FirstOrDefault();
            return subScope;
        }
        private static ScopeItem GetOrAddScope(IScope scope, string key) {
            var subScope = scope.Items.OfType<ScopeItem>().Where( i => i.Key == key ).FirstOrDefault();
            if (subScope == null) {
                subScope = new ScopeItem( key );
                scope.Items.Add( subScope );
            }
            return subScope;
        }

        // Helpers/GetString
        private static void GetString(StringBuilder builder, KeyValueTreeList<T> list) {
            builder.Append( "KeyValueTreeList:" );
            foreach (var item in list.Items) {
                GetString( builder, item.Key, item );
            }
        }
        private static void GetString(StringBuilder builder, string path, Item item) {
            if (item is ValueItem value) {
                builder.AppendLine();
                builder.Append( path ).Append( ": " ).Append( value.Value );
            } else
            if (item is ScopeItem scope) {
                foreach (var i in scope.Items) {
                    GetString( builder, $"{path}/{i.Key}", i );
                }
            }
        }

    }
}