Unless your ERC20 token contract has built into it the ability to iterate over the token holders (which is not a good idea nor gas efficient), there is no way to make a list from data available from the contract. This leaves a third party solution (EtherScan -- bad because centralized) or a solution that asks the node for the data.
There are two solutions here: (1) listen for mint events, or (2) find all the transactions to the contract and build the list from these transactions.
In the first case, if you listen for the mint events, you may find this doesn't work depending on the contract. The ERC20 standard says a contract SHOULD emit events when tokens are created or destroyed, but it doesn't say the contract MUST do so. If your contract does generate events, then this is probably the easiest way to get the list you're looking for. But note, that you have to watch for later transfers if you want to keep an up to date list.
The second option, if you don't want to use a third-party, is to find all the transactions on the contract. This is difficult because finding transactions is not easy because of things like multi-sig wallets.
At QuickBlocks we take a different approach. What we do is scan the blocks looking for transactions (both internal and external) looking for any address that ever interacted with the token contract. At this first pass, we don't care in what way the address interacted, just that it was involved in some transaction with the token.
Given this list, we then spin through that list and request the balance for that address and report the non-zero token balances. It's different, but pretty effective.
Here's a video explaining what I'm talking about: https://m.youtube.com/watch?v=c_KNulh3PF4